modules/presets/index.js (422 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';
import { data } from '../../data/index';
import { osmNodeGeometriesForTags } from '../osm/tags';
import { presetCategory } from './category';
import { presetCollection } from './collection';
import { presetField } from './field';
import { presetPreset } from './preset';
import { utilArrayUniq, utilRebind } from '../util';
import { groupManager } from '../entities/group_manager';
export { presetCategory };
export { presetCollection };
export { presetField };
export { presetPreset };
export function presetIndex(context) {
// a presetCollection with methods for
// loading new data and returning defaults
var dispatch = d3_dispatch('recentsChange', 'favoritePreset');
var all = presetCollection([]);
var _defaults = { area: all, line: all, point: all, vertex: all, relation: all };
var _fields = {};
var _universal = [];
var _favorites, _recents;
// presets that the user can add
var _addablePresetIDs;
// Index of presets by (geometry, tag key).
var _index = {
point: {},
vertex: {},
line: {},
area: {},
relation: {}
};
all.match = function(entity, resolver) {
return resolver.transient(entity, 'presetMatch', function() {
var geometry = entity.geometry(resolver);
// Treat entities on addr:interpolation lines as points, not vertices - #3241
if (geometry === 'vertex' && entity.isOnAddressLine(resolver)) {
geometry = 'point';
}
return all.matchTags(entity.tags, geometry);
});
};
all.matchTags = function(tags, geometry) {
var address;
var geometryMatches = _index[geometry];
var best = -1;
var match;
for (var k in tags) {
// If any part of an address is present,
// allow fallback to "Address" preset - #4353
if (/^addr:/.test(k) && geometryMatches['addr:*']) {
address = geometryMatches['addr:*'][0];
}
var keyMatches = geometryMatches[k];
if (!keyMatches) continue;
for (var i = 0; i < keyMatches.length; i++) {
var score = keyMatches[i].matchScore(tags);
if (score > best) {
best = score;
match = keyMatches[i];
}
}
}
if (address && (!match || match.isFallback())) {
match = address;
}
return match || all.fallback(geometry);
};
all.allowsVertex = function(entity, resolver) {
if (entity.type !== 'node') return false;
if (Object.keys(entity.tags).length === 0) return true;
return resolver.transient(entity, 'vertexMatch', function() {
// address lines allow vertices to act as standalone points
if (entity.isOnAddressLine(resolver)) return true;
var geometries = osmNodeGeometriesForTags(entity.tags);
if (geometries.vertex) return true;
if (geometries.point) return false;
// allow vertices for unspecified points
return true;
});
};
// Because of the open nature of tagging, iD will never have a complete
// list of tags used in OSM, so we want it to have logic like "assume
// that a closed way with an amenity tag is an area, unless the amenity
// is one of these specific types". This function computes a structure
// that allows testing of such conditions, based on the presets designated
// as as supporting (or not supporting) the area geometry.
//
// The returned object L is a whitelist/blacklist of tags. A closed way
// with a tag (k, v) is considered to be an area if `k in L && !(v in L[k])`
// (see `Way#isArea()`). In other words, the keys of L form the whitelist,
// and the subkeys form the blacklist.
all.areaKeys = function() {
var areaKeys = {};
var ignore = ['barrier', 'highway', 'footway', 'railway', 'junction', 'type']; // probably a line..
// ignore name-suggestion-index and deprecated presets
var presets = all.collection.filter(function(p) {
return !p.suggestion && !p.replacement;
});
// whitelist
presets.forEach(function(d) {
for (var key in d.tags) break;
if (!key) return;
if (ignore.indexOf(key) !== -1) return;
if (d.geometry.indexOf('area') !== -1) { // probably an area..
areaKeys[key] = areaKeys[key] || {};
}
});
// blacklist
presets.forEach(function(d) {
for (var key in d.addTags) {
// examine all addTags to get a better sense of what can be tagged on lines - #6800
var value = d.addTags[key];
if (key in areaKeys && // probably an area...
d.geometry.indexOf('line') !== -1 && // but sometimes a line
value !== '*') {
areaKeys[key][value] = true;
}
}
});
return areaKeys;
};
all.pointTags = function() {
return all.collection.reduce(function(pointTags, d) {
// ignore name-suggestion-index, deprecated, and generic presets
if (d.suggestion || d.replacement || d.searchable === false) return pointTags;
// only care about the primary tag
for (var key in d.tags) break;
if (!key) return pointTags;
// if this can be a point
if (d.geometry.indexOf('point') !== -1) {
pointTags[key] = pointTags[key] || {};
pointTags[key][d.tags[key]] = true;
}
return pointTags;
}, {});
};
all.vertexTags = function() {
return all.collection.reduce(function(vertexTags, d) {
// ignore name-suggestion-index, deprecated, and generic presets
if (d.suggestion || d.replacement || d.searchable === false) return vertexTags;
// only care about the primary tag
for (var key in d.tags) break;
if (!key) return vertexTags;
// if this can be a vertex
if (d.geometry.indexOf('vertex') !== -1) {
vertexTags[key] = vertexTags[key] || {};
vertexTags[key][d.tags[key]] = true;
}
return vertexTags;
}, {});
};
all.build = function(d, addable) {
if (d.fields) {
Object.keys(d.fields).forEach(function(id) {
var f = d.fields[id];
_fields[id] = presetField(id, f);
if (f.universal) {
_universal.push(_fields[id]);
}
});
}
if (d.presets) {
var rawPresets = d.presets;
Object.keys(d.presets).forEach(function(id) {
var p = d.presets[id];
var existing = all.index(id);
var isAddable = typeof addable === 'function' ? addable(id, p) : addable;
if (existing !== -1) {
all.collection[existing] = presetPreset(id, p, _fields, isAddable, rawPresets);
} else {
all.collection.push(presetPreset(id, p, _fields, isAddable, rawPresets));
}
});
}
if (d.categories) {
Object.keys(d.categories).forEach(function(id) {
var c = d.categories[id];
var existing = all.index(id);
if (existing !== -1) {
all.collection[existing] = presetCategory(id, c, all);
} else {
all.collection.push(presetCategory(id, c, all));
}
});
}
var getItem = (all.item).bind(all);
if (_addablePresetIDs) {
['area', 'line', 'point', 'vertex', 'relation'].forEach(function(geometry) {
_defaults[geometry] = presetCollection(_addablePresetIDs.map(getItem).filter(function(preset) {
return preset.geometry.indexOf(geometry) !== -1;
}));
});
} else if (d.defaults) {
_defaults = {
area: presetCollection(d.defaults.area.map(getItem)),
line: presetCollection(d.defaults.line.map(getItem)),
point: presetCollection(d.defaults.point.map(getItem)),
vertex: presetCollection(d.defaults.vertex.map(getItem)),
relation: presetCollection(d.defaults.relation.map(getItem))
};
}
for (var i = 0; i < all.collection.length; i++) {
var preset = all.collection[i];
var geometry = preset.geometry;
for (var j = 0; j < geometry.length; j++) {
var g = _index[geometry[j]];
for (var k in preset.tags) {
(g[k] = g[k] || []).push(preset);
}
}
}
return all;
};
all.init = function(addablePresetIDs) {
all.collection = [];
_favorites = null;
_recents = null;
_addablePresetIDs = addablePresetIDs;
_fields = {};
_universal = [];
_index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} };
var addable = true;
if (addablePresetIDs) {
addable = function(presetID) {
return addablePresetIDs.indexOf(presetID) !== -1;
};
}
return all.build(data.presets, addable);
};
all.reset = function() {
all.collection = [];
_defaults = { area: all, line: all, point: all, vertex: all, relation: all };
_fields = {};
_universal = [];
_favorites = null;
_recents = null;
groupManager.clearCachedPresets();
// Index of presets by (geometry, tag key).
_index = {
point: {},
vertex: {},
line: {},
area: {},
relation: {}
};
return all;
};
all.fromExternal = function(external, done) {
all.reset();
d3_json(external)
.then(function(externalPresets) {
all.build(data.presets, false); // load the default presets as non-addable to start
_addablePresetIDs = externalPresets.presets && Object.keys(externalPresets.presets);
all.build(externalPresets, true); // then load the external presets as addable
})
.catch(function() {
all.init();
})
.finally(function() {
done(all);
});
};
all.field = function(id) {
return _fields[id];
};
all.universal = function() {
return _universal;
};
all.defaults = function(geometry, n) {
var rec = [];
if (!context.inIntro()) {
rec = all.recent().matchGeometry(geometry).collection.slice(0, 4);
}
var def = utilArrayUniq(rec.concat(_defaults[geometry].collection)).slice(0, n - 1);
return presetCollection(utilArrayUniq(rec.concat(def).concat(all.fallback(geometry))));
};
all.recent = function() {
return presetCollection(utilArrayUniq(all.getRecents().map(function(d) {
return d.preset;
})));
};
function RibbonItem(preset, source) {
var item = {};
item.preset = preset;
item.source = source;
item.isFavorite = function() {
return item.source === 'favorite';
};
item.isRecent = function() {
return item.source === 'recent';
};
item.matches = function(preset) {
return item.preset.id === preset.id;
};
item.minified = function() {
return {
pID: item.preset.id
};
};
return item;
}
function ribbonItemForMinified(d, source) {
if (d && d.pID) {
var preset = all.item(d.pID);
if (!preset) return null;
return RibbonItem(preset, source);
}
return null;
}
function setFavorites(items) {
_favorites = items;
var minifiedItems = items.map(function(d) { return d.minified(); });
context.storage('preset_favorites', JSON.stringify(minifiedItems));
// call update
dispatch.call('favoritePreset');
}
all.getGenericRibbonItems = function() {
return ['point', 'line', 'area'].map(function(id) {
return RibbonItem(all.item(id), 'generic');
});
};
all.getFavorites = function() {
if (!_favorites) {
// fetch from local storage
var rawFavorites = JSON.parse(context.storage('preset_favorites'));
if (!rawFavorites) {
rawFavorites = [];
context.storage('preset_favorites', JSON.stringify(rawFavorites));
}
_favorites = rawFavorites.reduce(function(output, d) {
var item = ribbonItemForMinified(d, 'favorite');
if (item && item.preset.addable()) output.push(item);
return output;
}, []);
}
return _favorites;
};
function setRecents(items) {
_recents = items;
var minifiedItems = items.map(function(d) { return d.minified(); });
context.storage('preset_recents', JSON.stringify(minifiedItems));
dispatch.call('recentsChange');
}
all.getAddable = function() {
if (!_addablePresetIDs) return [];
return _addablePresetIDs.map(function(id) {
var preset = all.item(id);
if (preset) {
return RibbonItem(preset, 'addable');
}
}).filter(Boolean);
};
all.getRecents = function() {
if (!_recents) {
// fetch from local storage
_recents = (JSON.parse(context.storage('preset_recents')) || [])
.reduce(function(output, d) {
var item = ribbonItemForMinified(d, 'recent');
if (item && item.preset.addable()) output.push(item);
return output;
}, []);
}
return _recents;
};
all.toggleFavorite = function(preset) {
var favs = all.getFavorites();
var favorite = all.favoriteMatching(preset);
if (favorite) {
favs.splice(favs.indexOf(favorite), 1);
} else {
// only allow 10 favorites
if (favs.length === 10) {
// remove the last favorite (last in, first out)
favs.pop();
}
// append array
favs.push(RibbonItem(preset, 'favorite'));
}
setFavorites(favs);
};
all.removeFavorite = function(preset) {
var item = all.favoriteMatching(preset);
if (item) {
var items = all.getFavorites();
items.splice(items.indexOf(item), 1);
setFavorites(items);
}
};
all.removeRecent = function(preset) {
var item = all.recentMatching(preset);
if (item) {
var items = all.getRecents();
items.splice(items.indexOf(item), 1);
setRecents(items);
}
};
all.favoriteMatching = function(preset) {
var favs = all.getFavorites();
for (var index in favs) {
if (favs[index].matches(preset)) {
return favs[index];
}
}
return null;
};
all.recentMatching = function(preset) {
var items = all.getRecents();
for (var index in items) {
if (items[index].matches(preset)) {
return items[index];
}
}
return null;
};
all.moveItem = function(items, fromIndex, toIndex) {
if (fromIndex === toIndex ||
fromIndex < 0 || toIndex < 0 ||
fromIndex >= items.length || toIndex >= items.length) return null;
items.splice(toIndex, 0, items.splice(fromIndex, 1)[0]);
return items;
};
all.addFavorite = function(preset, besidePreset, after) {
var favorites = all.getFavorites();
var beforeItem = all.favoriteMatching(besidePreset);
var toIndex = favorites.indexOf(beforeItem);
if (after) toIndex += 1;
var newItem = RibbonItem(preset, 'favorite');
favorites.splice(toIndex, 0, newItem);
setFavorites(favorites);
};
all.addRecent = function(preset, besidePreset, after) {
var recents = all.getRecents();
var beforeItem = all.recentMatching(besidePreset);
var toIndex = recents.indexOf(beforeItem);
if (after) toIndex += 1;
var newItem = RibbonItem(preset, 'recent');
recents.splice(toIndex, 0, newItem);
setRecents(recents);
};
all.setMostRecent = function(preset) {
if (context.inIntro()) return;
if (preset.searchable === false) return;
var items = all.getRecents();
var item = all.recentMatching(preset);
if (item) {
items.splice(items.indexOf(item), 1);
} else {
item = RibbonItem(preset, 'recent');
}
// allow 30 recents
if (items.length === 30) {
// remove the last favorite (first in, first out)
items.pop();
}
// prepend array
items.unshift(item);
setRecents(items);
};
return utilRebind(all, dispatch, 'on');
}