diff --git a/data/index.js b/data/index.js index a0c8ab434..77f936b4c 100644 --- a/data/index.js +++ b/data/index.js @@ -1,16 +1,4 @@ export { dataLocales } from './locales.json'; export { en as dataEn } from '../dist/locales/en.json'; -import { presets } from './presets/presets.json'; -import { defaults } from './presets/defaults.json'; -import { categories } from './presets/categories.json'; -import { fields } from './presets/fields.json'; - -export let data = { - presets: { - presets: presets, - defaults: defaults, - categories: categories, - fields: fields - } -}; +export let data = {}; diff --git a/data/presets/categories.json b/data/presets/categories.json index d3d45f5b0..f4aeba1d5 100644 --- a/data/presets/categories.json +++ b/data/presets/categories.json @@ -1,226 +1,224 @@ { - "categories": { - "category-barrier": { - "icon": "maki-roadblock", - "name": "Barrier Features", - "members": [ - "barrier/fence", - "barrier/wall", - "barrier/ditch", - "barrier/gate", - "barrier/hedge", - "barrier/kerb", - "barrier" - ] - }, - "category-building": { - "icon": "maki-building", - "name": "Building Features", - "members": [ - "building", - "building/house", - "building/apartments", - "building/garage", - "building/retail", - "building/commercial", - "building/industrial", - "building/residential" - ] - }, - "category-golf": { - "icon": "maki-golf", - "name": "Golf Features", - "members": [ - "golf/fairway", - "golf/green", - "golf/lateral_water_hazard", - "golf/rough", - "golf/bunker", - "golf/tee", - "golf/water_hazard", - "golf/driving_range", - "golf/hole", - "golf/cartpath", - "golf/path" - ] - }, - "category-landuse": { - "icon": "maki-landuse", - "name": "Land Use Features", - "members": [ - "landuse/residential", - "landuse/industrial", - "landuse/commercial", - "landuse/retail", - "landuse/farmland", - "landuse/farmyard", - "landuse/forest", - "landuse/meadow", - "landuse/aquaculture", - "landuse/cemetery", - "landuse/military", - "landuse/religious" - ] - }, - "category-natural": { - "icon": "maki-natural", - "name": "Natural Features", - "members": [ - "natural/water", - "natural/wood", - "natural/scrub", - "natural/wetland", - "natural/grassland", - "natural/heath", - "natural/bare_rock", - "natural/beach", - "natural/cave_entrance", - "natural/glacier", - "natural/coastline", - "natural/tree_row", - "natural/peak", - "natural/cliff" - ] - }, - "category-path": { - "icon": "temaki-pedestrian", - "name": "Paths", - "members": [ - "highway/path", - "highway/footway", - "highway/footway/marked", - "highway/footway/sidewalk", - "highway/steps", - "highway/cycleway", - "highway/bridleway", - "highway/pedestrian_line" - ] - }, - "category-rail": { - "icon": "temaki-railway_track", - "name": "Rails", - "members": [ - "railway/rail", - "railway/disused", - "railway/tram", - "railway/subway", - "railway/narrow_gauge", - "railway/light_rail", - "railway/monorail", - "railway/funicular" - ] - }, - "category-restriction": { - "icon": "iD-restriction", - "name": "Restriction Features", - "members": [ - "type/restriction/no_left_turn", - "type/restriction/no_right_turn", - "type/restriction/no_straight_on", - "type/restriction/no_u_turn", - "type/restriction/only_left_turn", - "type/restriction/only_right_turn", - "type/restriction/only_straight_on", - "type/restriction/only_u_turn", - "type/restriction" - ] - }, - "category-road_major": { - "icon": "iD-highway-unclassified", - "name": "Major Roads", - "members": [ - "highway/motorway", - "highway/trunk", - "highway/primary", - "highway/secondary", - "highway/tertiary", - "highway/motorway_link", - "highway/trunk_link", - "highway/primary_link", - "highway/secondary_link", - "highway/tertiary_link" - ] - }, - "category-road_minor": { - "icon": "iD-highway-unclassified", - "name": "Minor Roads", - "members": [ - "highway/unclassified", - "highway/residential", - "highway/living_street", - "highway/service", - "highway/track" - ] - }, - "category-road_service": { - "icon": "iD-highway-service", - "name": "Service Roads", - "members": [ - "highway/service", - "highway/service/parking_aisle", - "highway/service/driveway", - "highway/service/alley", - "highway/service/emergency_access", - "highway/service/drive-through" - ] - }, - "category-route": { - "icon": "iD-route", - "name": "Route Features", - "members": [ - "type/route/road", - "type/route/bicycle", - "type/route/foot", - "type/route/hiking", - "type/route/horse", - "type/route/piste", - "type/route/bus", - "type/route/train", - "type/route/light_rail", - "type/route/tram", - "type/route/subway", - "type/route/ferry", - "type/route/power", - "type/route/pipeline", - "type/route/detour", - "type/route_master", - "type/route" - ] - }, - "category-utility": { - "icon": "iD-power-line", - "name": "Utility Features", - "members": [ - "power/line", - "power/minor_line", - "man_made/pipeline", - "power/cable/underground" - ] - }, - "category-water": { - "icon": "maki-water", - "name": "Water Bodies", - "members": [ - "natural/water", - "natural/water/pond", - "natural/water/basin", - "natural/water/lake", - "natural/water/reservoir" - ] - }, - "category-waterway": { - "icon": "iD-waterway-stream", - "name": "Waterways", - "members": [ - "waterway/stream", - "waterway/drain", - "waterway/river", - "waterway/canal", - "waterway/ditch", - "natural/water/stream", - "natural/water/river", - "natural/water/canal" - ] - } + "category-barrier": { + "icon": "maki-roadblock", + "name": "Barrier Features", + "members": [ + "barrier/fence", + "barrier/wall", + "barrier/ditch", + "barrier/gate", + "barrier/hedge", + "barrier/kerb", + "barrier" + ] + }, + "category-building": { + "icon": "maki-building", + "name": "Building Features", + "members": [ + "building", + "building/house", + "building/apartments", + "building/garage", + "building/retail", + "building/commercial", + "building/industrial", + "building/residential" + ] + }, + "category-golf": { + "icon": "maki-golf", + "name": "Golf Features", + "members": [ + "golf/fairway", + "golf/green", + "golf/lateral_water_hazard", + "golf/rough", + "golf/bunker", + "golf/tee", + "golf/water_hazard", + "golf/driving_range", + "golf/hole", + "golf/cartpath", + "golf/path" + ] + }, + "category-landuse": { + "icon": "maki-landuse", + "name": "Land Use Features", + "members": [ + "landuse/residential", + "landuse/industrial", + "landuse/commercial", + "landuse/retail", + "landuse/farmland", + "landuse/farmyard", + "landuse/forest", + "landuse/meadow", + "landuse/aquaculture", + "landuse/cemetery", + "landuse/military", + "landuse/religious" + ] + }, + "category-natural": { + "icon": "maki-natural", + "name": "Natural Features", + "members": [ + "natural/water", + "natural/wood", + "natural/scrub", + "natural/wetland", + "natural/grassland", + "natural/heath", + "natural/bare_rock", + "natural/beach", + "natural/cave_entrance", + "natural/glacier", + "natural/coastline", + "natural/tree_row", + "natural/peak", + "natural/cliff" + ] + }, + "category-path": { + "icon": "temaki-pedestrian", + "name": "Paths", + "members": [ + "highway/path", + "highway/footway", + "highway/footway/marked", + "highway/footway/sidewalk", + "highway/steps", + "highway/cycleway", + "highway/bridleway", + "highway/pedestrian_line" + ] + }, + "category-rail": { + "icon": "temaki-railway_track", + "name": "Rails", + "members": [ + "railway/rail", + "railway/disused", + "railway/tram", + "railway/subway", + "railway/narrow_gauge", + "railway/light_rail", + "railway/monorail", + "railway/funicular" + ] + }, + "category-restriction": { + "icon": "iD-restriction", + "name": "Restriction Features", + "members": [ + "type/restriction/no_left_turn", + "type/restriction/no_right_turn", + "type/restriction/no_straight_on", + "type/restriction/no_u_turn", + "type/restriction/only_left_turn", + "type/restriction/only_right_turn", + "type/restriction/only_straight_on", + "type/restriction/only_u_turn", + "type/restriction" + ] + }, + "category-road_major": { + "icon": "iD-highway-unclassified", + "name": "Major Roads", + "members": [ + "highway/motorway", + "highway/trunk", + "highway/primary", + "highway/secondary", + "highway/tertiary", + "highway/motorway_link", + "highway/trunk_link", + "highway/primary_link", + "highway/secondary_link", + "highway/tertiary_link" + ] + }, + "category-road_minor": { + "icon": "iD-highway-unclassified", + "name": "Minor Roads", + "members": [ + "highway/unclassified", + "highway/residential", + "highway/living_street", + "highway/service", + "highway/track" + ] + }, + "category-road_service": { + "icon": "iD-highway-service", + "name": "Service Roads", + "members": [ + "highway/service", + "highway/service/parking_aisle", + "highway/service/driveway", + "highway/service/alley", + "highway/service/emergency_access", + "highway/service/drive-through" + ] + }, + "category-route": { + "icon": "iD-route", + "name": "Route Features", + "members": [ + "type/route/road", + "type/route/bicycle", + "type/route/foot", + "type/route/hiking", + "type/route/horse", + "type/route/piste", + "type/route/bus", + "type/route/train", + "type/route/light_rail", + "type/route/tram", + "type/route/subway", + "type/route/ferry", + "type/route/power", + "type/route/pipeline", + "type/route/detour", + "type/route_master", + "type/route" + ] + }, + "category-utility": { + "icon": "iD-power-line", + "name": "Utility Features", + "members": [ + "power/line", + "power/minor_line", + "man_made/pipeline", + "power/cable/underground" + ] + }, + "category-water": { + "icon": "maki-water", + "name": "Water Bodies", + "members": [ + "natural/water", + "natural/water/pond", + "natural/water/basin", + "natural/water/lake", + "natural/water/reservoir" + ] + }, + "category-waterway": { + "icon": "iD-waterway-stream", + "name": "Waterways", + "members": [ + "waterway/stream", + "waterway/drain", + "waterway/river", + "waterway/canal", + "waterway/ditch", + "natural/water/stream", + "natural/water/river", + "natural/water/canal" + ] } } \ No newline at end of file diff --git a/data/presets/defaults.json b/data/presets/defaults.json index 6b0214543..03842a6c7 100644 --- a/data/presets/defaults.json +++ b/data/presets/defaults.json @@ -1,63 +1,61 @@ { - "defaults": { - "area": [ - "category-landuse", - "category-building", - "category-water", - "category-natural", - "leisure/park", - "amenity/hospital", - "amenity/place_of_worship", - "amenity/cafe", - "amenity/restaurant", - "area" - ], - "line": [ - "category-road_major", - "category-road_minor", - "category-rail", - "category-path", - "category-waterway", - "category-barrier", - "category-natural", - "category-utility", - "line" - ], - "point": [ - "category-natural", - "leisure/park", - "amenity/hospital", - "amenity/place_of_worship", - "amenity/cafe", - "amenity/restaurant", - "amenity/fast_food", - "amenity/bar", - "amenity/bank", - "shop/supermarket", - "point" - ], - "vertex": [ - "highway/crossing/marked", - "highway/crossing/unmarked", - "railway/level_crossing", - "highway/traffic_signals", - "highway/turning_circle", - "highway/turning_loop", - "traffic_calming", - "highway/mini_roundabout", - "highway/motorway_junction", - "point" - ], - "relation": [ - "category-route", - "category-restriction", - "public_transport/stop_area", - "type/boundary", - "type/waterway", - "type/multipolygon", - "type/enforcement", - "type/site", - "relation" - ] - } + "area": [ + "category-landuse", + "category-building", + "category-water", + "category-natural", + "leisure/park", + "amenity/hospital", + "amenity/place_of_worship", + "amenity/cafe", + "amenity/restaurant", + "area" + ], + "line": [ + "category-road_major", + "category-road_minor", + "category-rail", + "category-path", + "category-waterway", + "category-barrier", + "category-natural", + "category-utility", + "line" + ], + "point": [ + "category-natural", + "leisure/park", + "amenity/hospital", + "amenity/place_of_worship", + "amenity/cafe", + "amenity/restaurant", + "amenity/fast_food", + "amenity/bar", + "amenity/bank", + "shop/supermarket", + "point" + ], + "vertex": [ + "highway/crossing/marked", + "highway/crossing/unmarked", + "railway/level_crossing", + "highway/traffic_signals", + "highway/turning_circle", + "highway/turning_loop", + "traffic_calming", + "highway/mini_roundabout", + "highway/motorway_junction", + "point" + ], + "relation": [ + "category-route", + "category-restriction", + "public_transport/stop_area", + "type/boundary", + "type/waterway", + "type/multipolygon", + "type/enforcement", + "type/site", + "relation" + ] } diff --git a/modules/core/context.js b/modules/core/context.js index 5a54985a1..7d2f27550 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -549,26 +549,30 @@ export function coreContext() { _features.init(); _photos.init(); - let presetsParameter = hash.presets; - if (presetsParameter && presetsParameter.indexOf('://') !== -1) { - // a URL of external presets file - _presets.fromExternal(external, (externalPresets) => { - context.presets = () => externalPresets; // default + external presets... - osmSetAreaKeys(_presets.areaKeys()); - osmSetPointTags(_presets.pointTags()); - osmSetVertexTags(_presets.vertexTags()); - }); - } else { - let addablePresetIDs; - if (presetsParameter) { - // a list of allowed preset IDs - addablePresetIDs = presetsParameter.split(','); - } - _presets.init(addablePresetIDs); - osmSetAreaKeys(_presets.areaKeys()); - osmSetPointTags(_presets.pointTags()); - osmSetVertexTags(_presets.vertexTags()); - } + // let presetsParameter = hash.presets; + // if (presetsParameter && presetsParameter.indexOf('://') !== -1) { + // // a URL of external presets file + // _presets.fromExternal(external, (externalPresets) => { + // context.presets = () => externalPresets; // default + external presets... + // osmSetAreaKeys(_presets.areaKeys()); + // osmSetPointTags(_presets.pointTags()); + // osmSetVertexTags(_presets.vertexTags()); + // }); + // } else { + // let addablePresetIDs; + // if (presetsParameter) { + // // a list of allowed preset IDs + // addablePresetIDs = presetsParameter.split(','); + // } + // _presets.init(addablePresetIDs); + _presets.init() + .then(() => { + osmSetAreaKeys(_presets.areaKeys()); + osmSetPointTags(_presets.pointTags()); + osmSetVertexTags(_presets.vertexTags()); + }); + // } + return context; }; diff --git a/modules/core/data.js b/modules/core/data.js index 7a72fc899..6a833ecde 100644 --- a/modules/core/data.js +++ b/modules/core/data.js @@ -6,7 +6,7 @@ import { data as _data } from '../../data'; // prebundled data // The coreData module fetches data from JSON files // export function coreData(context) { - let _module = {}; + let _this = {}; let _inflight = {}; let _fileMap = { 'address_formats': 'data/address_formats.min.json', @@ -20,6 +20,10 @@ export function coreData(context) { 'nsi_filters': 'https://cdn.jsdelivr.net/npm/name-suggestion-index@3/dist/filters.min.json', 'oci_features': 'https://cdn.jsdelivr.net/npm/osm-community-index@2/dist/features.min.json', 'oci_resources': 'https://cdn.jsdelivr.net/npm/osm-community-index@2/dist/resources.min.json', + 'preset_categories': 'data/preset_categories.min.json', + 'preset_defaults': 'data/preset_defaults.min.json', + 'preset_fields': 'data/fields.min.json', + 'preset_presets': 'data/presets.min.json', 'phone_formats': 'data/phone_formats.min.json', 'shortcuts': 'data/shortcuts.min.json', 'territory_languages': 'data/territory_languages.min.json', @@ -29,7 +33,7 @@ export function coreData(context) { // Returns a Promise to fetch data // (resolved with the data if we have it already) - _module.get = (which) => { + _this.get = (which) => { if (_data[which]) { return Promise.resolve(_data[which]); } @@ -62,12 +66,12 @@ export function coreData(context) { // Accessor for the file map - _module.fileMap = function(val) { + _this.fileMap = function(val) { if (!arguments.length) return _fileMap; _fileMap = val; - return _module; + return _this; }; - return _module; + return _this; } diff --git a/modules/operations/downgrade.js b/modules/operations/downgrade.js index a8fb6fa5a..3ebc7d8d0 100644 --- a/modules/operations/downgrade.js +++ b/modules/operations/downgrade.js @@ -33,7 +33,7 @@ export function operationDowngrade(selectedIDs, context) { var entity = graph.entity(entityID); var preset = context.presets().match(entity, graph); - if (preset.isFallback()) return null; + if (!preset || preset.isFallback()) return null; if (entity.type === 'node' && preset.id !== 'address' && diff --git a/modules/presets/category.js b/modules/presets/category.js index cd5ce0913..622b72d66 100644 --- a/modules/presets/category.js +++ b/modules/presets/category.js @@ -2,53 +2,35 @@ import { t } from '../util/locale'; import { presetCollection } from './collection'; -export function presetCategory(id, category, all) { - category = Object.assign({}, category); // shallow copy +export function presetCategory(categoryID, category, all) { + let _this = Object.assign({}, category); // shallow copy - category.id = id; + _this.id = categoryID; + _this.members = presetCollection(_this.members.map(presetID => all.item(presetID))); - category.members = presetCollection(category.members.map(function(id) { - return all.item(id); - })); - - - category.geometry = category.members.collection.reduce(function(geometries, preset) { - for (var index in preset.geometry) { - var geometry = preset.geometry[index]; - if (geometries.indexOf(geometry) === -1) { - geometries.push(geometry); - } + _this.geometry = _this.members.collection + .reduce((acc, preset) => { + for (let i in preset.geometry) { + const geometry = preset.geometry[i]; + if (acc.indexOf(geometry) === -1) { + acc.push(geometry); } - return geometries; + } + return acc; }, []); + _this.matchGeometry = (geom) => _this.geometry.indexOf(geom) >= 0; - category.matchGeometry = function(geometry) { - return category.geometry.indexOf(geometry) >= 0; - }; + _this.matchAllGeometry = (geometries) => _this.members.collection + .some(preset => preset.matchAllGeometry(geometries)); - category.matchAllGeometry = function(geometries) { - return category.members.collection.some(function(preset) { - return preset.matchAllGeometry(geometries); - }); - }; + _this.matchScore = () => -1; + + _this.name = () => t(`presets.categories.${categoryID}.name`, { 'default': categoryID }); + + _this.terms = () => []; - category.matchScore = function() { - return -1; - }; - - - category.name = function() { - return t('presets.categories.' + id + '.name', {'default': id}); - }; - - - category.terms = function() { - return []; - }; - - - return category; + return _this; } diff --git a/modules/presets/collection.js b/modules/presets/collection.js index e13c0ea50..62cefa24e 100644 --- a/modules/presets/collection.js +++ b/modules/presets/collection.js @@ -2,187 +2,149 @@ import { utilArrayUniq, utilEditDistance } from '../util'; export function presetCollection(collection) { - var maxSearchResults = 50; + const MAXRESULTS = 50; - var presets = { + let _this = {}; - collection: collection, + _this.collection = collection; + + _this.item = (id) => _this.collection.find(d => d.id === id); + + _this.index = (id) => _this.collection.findIndex(d => d.id === id); + + _this.matchGeometry = (geometry) => { + return presetCollection( + _this.collection.filter(d => d.matchGeometry(geometry)) + ); + }; + + _this.matchAllGeometry = (geometries) => { + return presetCollection(_this.collection.filter(d => { + if (!d) return false; + return d.matchAllGeometry(geometries); + })); + }; + + _this.matchAnyGeometry = (geometries) => { + return presetCollection(_this.collection.filter(d => { + return geometries.some(geom => d.matchGeometry(geom)); + })); + }; + + _this.fallback = (geometry) => { + let id = geometry; + if (id === 'vertex') id = 'point'; + return _this.item(id); + }; + + _this.search = (value, geometry, countryCode) => { + if (!value) return this; + + value = value.toLowerCase().trim(); + + // match at name beginning or just after a space (e.g. "office" -> match "Law Office") + function leading(a) { + const index = a.indexOf(value); + return index === 0 || a[index - 1] === ' '; + } + + // match at name beginning only + function leadingStrict(a) { + const index = a.indexOf(value); + return index === 0; + } + + function sortNames(a, b) { + let aCompare = (a.suggestion ? a.originalName : a.name()).toLowerCase(); + let bCompare = (b.suggestion ? b.originalName : b.name()).toLowerCase(); + + // priority if search string matches preset name exactly - #4325 + if (value === aCompare) return -1; + if (value === bCompare) return 1; + + // priority for higher matchScore + let i = b.originalScore - a.originalScore; + if (i !== 0) return i; + + // priority if search string appears earlier in preset name + i = aCompare.indexOf(value) - bCompare.indexOf(value); + if (i !== 0) return i; + + // priority for shorter preset names + return aCompare.length - bCompare.length; + } + + let pool = _this.collection; + if (countryCode) { + pool = pool.filter(a => { + if (a.countryCodes && a.countryCodes.indexOf(countryCode) === -1) return false; + if (a.notCountryCodes && a.notCountryCodes.indexOf(countryCode) !== -1) return false; + return true; + }); + } + const searchable = pool.filter(a => a.searchable !== false && a.suggestion !== true); + const suggestions = pool.filter(a => a.suggestion === true); + + // matches value to preset.name + const leading_name = searchable + .filter(a => leading(a.name().toLowerCase())) + .sort(sortNames); + + // matches value to preset suggestion name (original name is unhyphenated) + const leading_suggestions = suggestions + .filter(a => leadingStrict(a.originalName.toLowerCase())) + .sort(sortNames); + + // matches value to preset.terms values + const leading_terms = searchable + .filter(a => (a.terms() || []).some(leading)); + + // matches value to preset.tags values + const leading_tag_values = searchable + .filter(a => Object.values(a.tags || {}).filter(val => val !== '*').some(leading)); + + // finds close matches to value in preset.name + const similar_name = searchable + .map(a => ({ preset: a, dist: utilEditDistance(value, a.name()) })) + .filter(a => a.dist + Math.min(value.length - a.preset.name().length, 0) < 3) + .sort((a, b) => a.dist - b.dist) + .map(a => a.preset); + + // finds close matches to value to preset suggestion name (original name is unhyphenated) + const similar_suggestions = suggestions + .map(a => ({ preset: a, dist: utilEditDistance(value, a.originalName.toLowerCase()) })) + .filter(a => a.dist + Math.min(value.length - a.preset.originalName.length, 0) < 1) + .sort((a, b) => a.dist - b.dist) + .map(a => a.preset); + + // finds close matches to value in preset.terms + const similar_terms = searchable + .filter(a => { + return (a.terms() || []).some(b => { + return utilEditDistance(value, b) + Math.min(value.length - b.length, 0) < 3; + }); + }); + + let results = leading_name.concat( + leading_suggestions, + leading_terms, + leading_tag_values, + similar_name, + similar_suggestions, + similar_terms + ).slice(0, MAXRESULTS - 1); + + if (geometry) { + if (typeof geometry === 'string') { + results.push(_this.fallback(geometry)); + } else { + geometry.forEach(geom => results.push(_this.fallback(geom))); + } + } + + return presetCollection(utilArrayUniq(results)); + }; - item: function(id) { - return this.collection.find(function(d) { - return d.id === id; - }); - }, - - index: function(id) { - return this.collection.findIndex(function(d) { - return d.id === id; - }); - }, - - matchGeometry: function(geometry) { - return presetCollection(this.collection.filter(function(d) { - return d.matchGeometry(geometry); - })); - }, - - matchAllGeometry: function(geometries) { - return presetCollection(this.collection.filter(function(d) { - if (!d) return false; - return d.matchAllGeometry(geometries); - })); - }, - - matchAnyGeometry: function(geometries) { - return presetCollection(this.collection.filter(function(d) { - return geometries.some(function(geometry) { - return d.matchGeometry(geometry); - }); - })); - }, - - fallback: function(geometry) { - var id = geometry; - if (id === 'vertex') id = 'point'; - return this.item(id); - }, - - search: function(value, geometry, countryCode) { - if (!value) return this; - - value = value.toLowerCase().trim(); - - // match at name beginning or just after a space (e.g. "office" -> match "Law Office") - function leading(a) { - var index = a.indexOf(value); - return index === 0 || a[index - 1] === ' '; - } - - // match at name beginning only - function leadingStrict(a) { - var index = a.indexOf(value); - return index === 0; - } - - function sortNames(a, b) { - var aCompare = (a.suggestion ? a.originalName : a.name()).toLowerCase(); - var bCompare = (b.suggestion ? b.originalName : b.name()).toLowerCase(); - - // priority if search string matches preset name exactly - #4325 - if (value === aCompare) return -1; - if (value === bCompare) return 1; - - // priority for higher matchScore - var i = b.originalScore - a.originalScore; - if (i !== 0) return i; - - // priority if search string appears earlier in preset name - i = aCompare.indexOf(value) - bCompare.indexOf(value); - if (i !== 0) return i; - - // priority for shorter preset names - return aCompare.length - bCompare.length; - } - - var pool = this.collection; - if (countryCode) { - pool = pool.filter(function(a) { - if (a.countryCodes && a.countryCodes.indexOf(countryCode) === -1) { - return false; - } - if (a.notCountryCodes && a.notCountryCodes.indexOf(countryCode) !== -1) { - return false; - } - return true; - }); - } - var searchable = pool.filter(function(a) { - return a.searchable !== false && a.suggestion !== true; - }); - var suggestions = pool.filter(function(a) { - return a.suggestion === true; - }); - - // matches value to preset.name - var leading_name = searchable - .filter(function(a) { - return leading(a.name().toLowerCase()); - }).sort(sortNames); - - // matches value to preset.terms values - var leading_terms = searchable - .filter(function(a) { - return (a.terms() || []).some(leading); - }); - - // matches value to preset.tags values - var leading_tag_values = searchable - .filter(function(a) { - return Object.values(a.tags || {}) - .filter(function(val) { return val !== '*'; }) - .some(leading); - }); - - var leading_suggestions = suggestions - .filter(function(a) { - return leadingStrict(a.originalName.toLowerCase()); - }).sort(sortNames); - - // finds close matches to value in preset.name - var similar_name = searchable - .map(function(a) { - return { preset: a, dist: utilEditDistance(value, a.name()) }; - }).filter(function(a) { - return a.dist + Math.min(value.length - a.preset.name().length, 0) < 3; - }).sort(function(a, b) { - return a.dist - b.dist; - }).map(function(a) { - return a.preset; - }); - - // finds close matches to value in preset.terms - var similar_terms = searchable - .filter(function(a) { - return (a.terms() || []).some(function(b) { - return utilEditDistance(value, b) + Math.min(value.length - b.length, 0) < 3; - }); - }); - - var similar_suggestions = suggestions - .map(function(a) { - return { preset: a, dist: utilEditDistance(value, a.originalName.toLowerCase()) }; - }).filter(function(a) { - return a.dist + Math.min(value.length - a.preset.originalName.length, 0) < 1; - }).sort(function(a, b) { - return a.dist - b.dist; - }).map(function(a) { - return a.preset; - }); - - var results = leading_name.concat( - leading_suggestions, - leading_terms, - leading_tag_values, - similar_name, - similar_suggestions, - similar_terms - ).slice(0, maxSearchResults - 1); - - if (geometry) { - if (typeof geometry === 'string') { - results.push(presets.fallback(geometry)); - } else { - geometry.forEach(function(geom) { - results.push(presets.fallback(geom)); - }); - } - } - - return presetCollection(utilArrayUniq(results)); - } - }; - - - return presets; + return _this; } diff --git a/modules/presets/field.js b/modules/presets/field.js index b91937442..60f527bfc 100644 --- a/modules/presets/field.js +++ b/modules/presets/field.js @@ -1,47 +1,33 @@ import { t } from '../util/locale'; import { utilSafeClassName } from '../util/util'; -export function presetField(id, field) { - field = Object.assign({}, field); // shallow copy - field.id = id; +export function presetField(fieldID, field) { + let _this = Object.assign({}, field); // shallow copy - // for use in classes, element ids, css selectors - field.safeid = utilSafeClassName(id); + _this.id = fieldID; - field.matchGeometry = function(geometry) { - return !field.geometry || field.geometry === geometry; - }; + // for use in classes, element ids, css selectors + _this.safeid = utilSafeClassName(fieldID); - field.matchAllGeometry = function(geometries) { - return !field.geometry || geometries.every(function(geometry) { - return field.geometry.indexOf(geometry) !== -1; - }); - }; + _this.matchGeometry = (geom) => !_this.geometry || _this.geometry === geom; + + _this.matchAllGeometry = (geometries) => { + return !_this.geometry || geometries.every(geom => _this.geometry.indexOf(geom) !== -1); + }; + + _this.t = (scope, options) => t(`presets.fields.${fieldID}.${scope}`, options); + + _this.label = () => _this.overrideLabel || _this.t('label', { 'default': fieldID }); + + const _placeholder = _this.placeholder; + _this.placeholder = () => _this.t('placeholder', { 'default': _placeholder }); + + _this.originalTerms = (_this.terms || []).join(); + + _this.terms = () => _this.t('terms', { 'default': _this.originalTerms }) + .toLowerCase().trim().split(/\s*,+\s*/); - field.t = function(scope, options) { - return t('presets.fields.' + id + '.' + scope, options); - }; - - - field.label = function() { - return field.overrideLabel || field.t('label', {'default': id}); - }; - - - var placeholder = field.placeholder; - field.placeholder = function() { - return field.t('placeholder', {'default': placeholder}); - }; - - - field.originalTerms = (field.terms || []).join(); - - field.terms = function() { - return field.t('terms', { 'default': field.originalTerms }).toLowerCase().trim().split(/\s*,+\s*/); - }; - - - return field; + return _this; } diff --git a/modules/presets/index.js b/modules/presets/index.js index 0fa5c039d..213dfd544 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -15,440 +15,468 @@ export { presetField }; export { presetPreset }; +// wraps a presetCollection with methods for +// loading new data and returning defaults export function presetIndex(context) { - // a presetCollection with methods for - // loading new data and returning defaults + const dispatch = d3_dispatch('recentsChange'); + const MAXRECENTS = 30; + let _presetData; - var dispatch = d3_dispatch('recentsChange'); + let _this = presetCollection([]); // collection of all presets + let _defaults = { + point: presetCollection([]), + vertex: presetCollection([]), + line: presetCollection([]), + area: presetCollection([]), + relation: presetCollection([]) + }; - var all = presetCollection([]); - var _defaults = { area: all, line: all, point: all, vertex: all, relation: all }; - var _fields = {}; - var _universal = []; - var _recents; - // presets that the user can add - var _addablePresetIDs; + let _fields = {}; + let _universal = []; + let _recents; + // let _addablePresetIDs; // presets that the user can add - // 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; - }); - }; + // Index of presets by (geometry, tag key). + let _index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; - // 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.. + function ensurePresetData() { + const data = context.data(); + return Promise.all([ + data.get('preset_categories'), + data.get('preset_defaults'), + data.get('preset_presets'), + data.get('preset_fields') + ]) + .then(vals => { + if (_presetData) return _presetData; - // 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 = []; - _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 = []; - _recents = null; - - // Index of presets by (geometry, tag key). - _index = { - point: {}, - vertex: {}, - line: {}, - area: {}, - relation: {} + return _presetData = { + categories: vals[0], + defaults: vals[1], + presets: vals[2], + fields: vals[3] }; + }); + } - return all; + + // _this.init = (addablePresetIDs) => { + _this.init = () => { + // _addablePresetIDs = addablePresetIDs; + _this.collection = []; + _recents = null; + _fields = {}; + _universal = []; + _index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; + + // let addable = true; + // if (addablePresetIDs) { + // addable = (presetID) => addablePresetIDs.indexOf(presetID) !== -1; + // } + // return _this.build(d, addable); + + return ensurePresetData() + .then(d => _this.build(d)); + }; + + + _this.reset = () => { + _defaults = { + point: presetCollection([]), + vertex: presetCollection([]), + line: presetCollection([]), + area: presetCollection([]), + relation: presetCollection([]) }; - 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 + _this.collection = []; + _recents = null; + _fields = {}; + _universal = []; + _index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; - _addablePresetIDs = externalPresets.presets && Object.keys(externalPresets.presets); + return _this; + }; - 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]; - }; + // _this.fromExternal = (external, done) => { + // _this.reset(); + // d3_json(external) + // .then(externalPresets => { + // _this.build(data.presets, false); // load the default presets as non-addable to start + // _addablePresetIDs = externalPresets.presets && Object.keys(externalPresets.presets); + // _this.build(externalPresets, true); // then load the external presets as addable + // }) + // .catch(() => _this.init()) + // .finally(() => done(_this)); + // }; - all.universal = function() { - return _universal; - }; - - all.defaults = function(geometry, n) { - var rec = []; - if (!context.inIntro()) { - rec = all.recent().matchGeometry(geometry).collection.slice(0, 4); + // _this.build = (d, addable) => { + _this.build = (d) => { + if (d.fields && Object.keys(d.fields).length) { + Object.keys(d.fields).forEach(fieldID => { + const f = d.fields[fieldID]; + _fields[fieldID] = presetField(fieldID, f); + if (f.universal) { + _universal.push(_fields[fieldID]); } - 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, geometry, source) { - var item = {}; - item.preset = preset; - item.geometry = geometry; - item.source = source; - - item.isRecent = function() { - return item.source === 'recent'; - }; - item.matches = function(preset, geometry) { - return item.preset.id === preset.id && item.geometry === geometry; - }; - item.minified = function() { - return { - pID: item.preset.id, - geom: item.geometry - }; - }; - return item; + }); } - function ribbonItemForMinified(d, source) { - if (d && d.pID && d.geom) { - var preset = all.item(d.pID); - if (!preset) return null; - - var geom = d.geom; - // treat point and vertex features as one geometry - if (geom === 'vertex') geom = 'point'; - - // iD's presets could have changed since this was saved, - // so make sure it's still valid. - if (preset.matchGeometry(geom) || (geom === 'point' && preset.matchGeometry('vertex'))) { - return RibbonItem(preset, geom, source); - } - } - return null; - } - - 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.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.removeRecent = function(preset, geometry) { - var item = all.recentMatching(preset, geometry); - if (item) { - var items = all.getRecents(); - items.splice(items.indexOf(item), 1); - setRecents(items); - } - }; - - all.recentMatching = function(preset, geometry) { - geometry = all.fallback(geometry).id; - var items = all.getRecents(); - for (var index in items) { - if (items[index].matches(preset, geometry)) { - 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.moveRecent = function(item, beforeItem) { - var recents = all.getRecents(); - var fromIndex = recents.indexOf(item); - var toIndex = recents.indexOf(beforeItem); - var items = all.moveItem(recents, fromIndex, toIndex); - if (items) setRecents(items); - }; - - all.setMostRecent = function(preset, geometry) { - if (context.inIntro()) return; - if (preset.searchable === false) return; - - geometry = all.fallback(geometry).id; - - var items = all.getRecents(); - var item = all.recentMatching(preset, geometry); - if (item) { - items.splice(items.indexOf(item), 1); + if (d.presets && Object.keys(d.presets).length) { + const rawPresets = d.presets; + Object.keys(d.presets).forEach(presetID => { + const p = d.presets[presetID]; + const existing = _this.index(presetID); + // const isAddable = typeof addable === 'function' ? addable(presetID, p) : addable; + const isAddable = true; + if (existing !== -1) { + _this.collection[existing] = presetPreset(presetID, p, _fields, isAddable, rawPresets); } else { - item = RibbonItem(preset, geometry, 'recent'); + _this.collection.push(presetPreset(presetID, p, _fields, isAddable, rawPresets)); } - // allow 30 recents - if (items.length === 30) { - // remove the last recent (first in, first out) - items.pop(); - } - // prepend array - items.unshift(item); - setRecents(items); - }; + }); + } - return utilRebind(all, dispatch, 'on'); + if (d.categories && Object.keys(d.categories).length) { + Object.keys(d.categories).forEach(categoryID => { + const c = d.categories[categoryID]; + const existing = _this.index(categoryID); + if (existing !== -1) { + _this.collection[existing] = presetCategory(categoryID, c, _this); + } else { + _this.collection.push(presetCategory(categoryID, c, _this)); + } + }); + } + + const getItem = (_this.item).bind(_this); + // if (_addablePresetIDs) { + // ['area', 'line', 'point', 'vertex', 'relation'].forEach(geometry => { + // _defaults[geometry] = presetCollection( + // _addablePresetIDs.map(getItem).filter(preset => preset.geometry.indexOf(geometry) !== -1) + // ); + // }); + // } else if (d.defaults) { + if (d.defaults && Object.keys(d.defaults).length) { + _defaults = { + point: presetCollection(d.defaults.point.map(getItem)), + vertex: presetCollection(d.defaults.vertex.map(getItem)), + line: presetCollection(d.defaults.line.map(getItem)), + area: presetCollection(d.defaults.area.map(getItem)), + relation: presetCollection(d.defaults.relation.map(getItem)) + }; + } + + for (let i = 0; i < _this.collection.length; i++) { + const preset = _this.collection[i]; + const geometry = preset.geometry; + + for (let j = 0; j < geometry.length; j++) { + let g = _index[geometry[j]]; + for (let k in preset.tags) { + (g[k] = g[k] || []).push(preset); + } + } + } + return _this; + }; + + + _this.match = (entity, resolver) => { + return resolver.transient(entity, 'presetMatch', () => { + let geometry = entity.geometry(resolver); + // Treat entities on addr:interpolation lines as points, not vertices - #3241 + if (geometry === 'vertex' && entity.isOnAddressLine(resolver)) { + geometry = 'point'; + } + return _this.matchTags(entity.tags, geometry); + }); + }; + + + _this.matchTags = (tags, geometry) => { + const geometryMatches = _index[geometry]; + let address; + let best = -1; + let match; + + for (let 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]; + } + + const keyMatches = geometryMatches[k]; + if (!keyMatches) continue; + + for (let i = 0; i < keyMatches.length; i++) { + const score = keyMatches[i].matchScore(tags); + if (score > best) { + best = score; + match = keyMatches[i]; + } + } + } + + if (address && (!match || match.isFallback())) { + match = address; + } + return match || _this.fallback(geometry); + }; + + + _this.allowsVertex = (entity, resolver) => { + if (entity.type !== 'node') return false; + if (Object.keys(entity.tags).length === 0) return true; + + return resolver.transient(entity, 'vertexMatch', () => { + // address lines allow vertices to act as standalone points + if (entity.isOnAddressLine(resolver)) return true; + + const 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 keeplist/discardlist 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 keeplist, + // and the subkeys form the discardlist. + _this.areaKeys = () => { + // The ignore list is for keys that imply lines. (We always add `area=yes` for exceptions) + const ignore = ['barrier', 'highway', 'footway', 'railway', 'junction', 'type']; + let areaKeys = {}; + + // ignore name-suggestion-index and deprecated presets + const presets = _this.collection.filter(p => !p.suggestion && !p.replacement); + + // keeplist + presets.forEach(p => { + let key; + for (key in p.tags) break; // pick the first tag + if (!key) return; + if (ignore.indexOf(key) !== -1) return; + + if (p.geometry.indexOf('area') !== -1) { // probably an area.. + areaKeys[key] = areaKeys[key] || {}; + } + }); + + // discardlist + presets.forEach(p => { + let key; + for (key in p.addTags) { + // examine all addTags to get a better sense of what can be tagged on lines - #6800 + const value = p.addTags[key]; + if (key in areaKeys && // probably an area... + p.geometry.indexOf('line') !== -1 && // but sometimes a line + value !== '*') { + areaKeys[key][value] = true; + } + } + }); + + return areaKeys; + }; + + + _this.pointTags = () => { + return _this.collection.reduce((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 + let key; + for (key in d.tags) break; // pick the first tag + 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; + }, {}); + }; + + + _this.vertexTags = () => { + return _this.collection.reduce((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 + let key; + for (key in d.tags) break; // pick the first tag + 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; + }, {}); + }; + + + _this.field = (id) => _fields[id]; + + _this.universal = () => _universal; + + + _this.defaults = (geometry, n) => { + let rec = []; + if (!context.inIntro()) { + rec = _this.recent().matchGeometry(geometry).collection.slice(0, 4); + } + const def = utilArrayUniq(rec.concat(_defaults[geometry].collection)).slice(0, n - 1); + return presetCollection( + utilArrayUniq(rec.concat(def).concat(_this.fallback(geometry))) + ); + }; + + + _this.recent = () => { + return presetCollection( + utilArrayUniq(_this.getRecents().map(d => d.preset)) + ); + }; + + + function RibbonItem(preset, geometry, source) { + let item = {}; + item.preset = preset; + item.geometry = geometry; + item.source = source; + + item.isRecent = () => item.source === 'recent'; + item.matches = (preset, geometry) => item.preset.id === preset.id && item.geometry === geometry; + item.minified = () => ({ pID: item.preset.id, geom: item.geometry }); + + return item; + } + + + function ribbonItemForMinified(d, source) { + if (d && d.pID && d.geom) { + const preset = _this.item(d.pID); + if (!preset) return null; + + let geom = d.geom; + // treat point and vertex features as one geometry + if (geom === 'vertex') geom = 'point'; + + // iD's presets could have changed since this was saved, + // so make sure it's still valid. + if (preset.matchGeometry(geom) || (geom === 'point' && preset.matchGeometry('vertex'))) { + return RibbonItem(preset, geom, source); + } + } + return null; + } + + + function setRecents(items) { + _recents = items; + const minifiedItems = items.map(d => d.minified()); + context.storage('preset_recents', JSON.stringify(minifiedItems)); + dispatch.call('recentsChange'); + } + + + _this.getRecents = () => { + if (!_recents) { + // fetch from local storage + _recents = (JSON.parse(context.storage('preset_recents')) || []) + .reduce((acc, d) => { + let item = ribbonItemForMinified(d, 'recent'); + if (item && item.preset.addable()) acc.push(item); + return acc; + }, []); + } + return _recents; + }; + + + _this.removeRecent = (preset, geometry) => { + const item = _this.recentMatching(preset, geometry); + if (item) { + let items = _this.getRecents(); + items.splice(items.indexOf(item), 1); + setRecents(items); + } + }; + + + _this.recentMatching = (preset, geometry) => { + geometry = _this.fallback(geometry).id; + const items = _this.getRecents(); + for (let i in items) { + if (items[i].matches(preset, geometry)) { + return items[i]; + } + } + return null; + }; + + + _this.moveItem = (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; + }; + + + _this.moveRecent = (item, beforeItem) => { + const recents = _this.getRecents(); + const fromIndex = recents.indexOf(item); + const toIndex = recents.indexOf(beforeItem); + const items = _this.moveItem(recents, fromIndex, toIndex); + if (items) setRecents(items); + }; + + + _this.setMostRecent = (preset, geometry) => { + if (context.inIntro()) return; + if (preset.searchable === false) return; + + geometry = _this.fallback(geometry).id; + + let items = _this.getRecents(); + let item = _this.recentMatching(preset, geometry); + if (item) { + items.splice(items.indexOf(item), 1); + } else { + item = RibbonItem(preset, geometry, 'recent'); + } + + // remove the last recent (first in, first out) + while (items.length >= MAXRECENTS) { + items.pop(); + } + + // prepend array + items.unshift(item); + setRecents(items); + }; + + return utilRebind(_this, dispatch, 'on'); } diff --git a/modules/presets/preset.js b/modules/presets/preset.js index 28b2f8593..02c6fb598 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -4,291 +4,267 @@ import { utilArrayUniq, utilObjectOmit } from '../util'; import { utilSafeClassName } from '../util/util'; -export function presetPreset(id, preset, fields, addable, rawPresets) { - preset = Object.assign({}, preset); // shallow copy +export function presetPreset(presetID, preset, fields, addable, rawPresets) { + let _this = Object.assign({}, preset); // shallow copy - preset.id = id; + _this.id = presetID; - // for use in classes, element ids, css selectors - preset.safeid = utilSafeClassName(id); + // for use in classes, element ids, css selectors + _this.safeid = utilSafeClassName(presetID); - preset.parentPresetID = function() { - var endIndex = preset.id.lastIndexOf('/'); - if (endIndex < 0) return null; - - return preset.id.substring(0, endIndex); - }; + _this.parentPresetID = () => { + const endIndex = _this.id.lastIndexOf('/'); + if (endIndex < 0) return null; + return _this.id.substring(0, endIndex); + }; - // For a preset without fields, use the fields of the parent preset. - // Replace {preset} placeholders with the fields of the specified presets. - function resolveFieldInheritance() { + // For a preset without fields, use the fields of the parent _this. + // Replace {preset} placeholders with the fields of the specified presets. + function resolveFieldInheritance() { - // Skip `fields` for the keys which define the preset. - // These are usually `typeCombo` fields like `shop=*` - function shouldInheritFieldWithID(fieldID) { - var f = fields[fieldID]; - if (f.key) { - if (preset.tags[f.key] !== undefined && - // inherit anyway if multiple values are allowed or just a checkbox - f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'check') { - return false; - } + // Skip `fields` for the keys which define the _this. + // These are usually `typeCombo` fields like `shop=*` + function shouldInheritFieldWithID(fieldID) { + const f = fields[fieldID]; + if (f.key) { + if (_this.tags[f.key] !== undefined && + // inherit anyway if multiple values are allowed or just a checkbox + f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'check' + ) return false; + } + return true; + } + + // returns an array of field IDs to inherit from the given presetID, if found + function inheritedFieldIDs(presetID, prop) { + if (!presetID) return null; + + const inheritPreset = rawPresets[presetID]; + if (!inheritPreset) return null; + + let inheritFieldIDs = inheritPreset[prop] || []; + if (prop === 'fields') { + inheritFieldIDs = inheritFieldIDs.filter(shouldInheritFieldWithID); + } + + return inheritFieldIDs; + } + + + ['fields', 'moreFields'].forEach(prop => { + let fieldIDs = []; + if (preset[prop] && preset[prop].length) { // fields were defined + preset[prop].forEach(fieldID => { + const match = fieldID.match(/\{(.*)\}/); + if (match !== null) { // presetID wrapped in braces {} + const inheritIDs = inheritedFieldIDs(match[1], prop); + if (inheritIDs !== null) { + fieldIDs = fieldIDs.concat(inheritIDs); + } else { + /* eslint-disable no-console */ + console.log(`Cannot resolve presetID ${match[0]} found in ${_this.id} ${prop}`); + /* eslint-enable no-console */ } - return true; - } - - // returns an array of field IDs to inherit from the given presetID, if found - function inheritedFieldIDs(presetID, prop) { - if (!presetID) return null; - - var inheritPreset = rawPresets[presetID]; - if (!inheritPreset) return null; - - var inheritFieldIDs = inheritPreset[prop] || []; - - if (prop === 'fields') { - inheritFieldIDs = inheritFieldIDs.filter(shouldInheritFieldWithID); - } - - return inheritFieldIDs; - } - - - ['fields', 'moreFields'].forEach(function(prop) { - var fieldIDs = []; - if (preset[prop] && preset[prop].length) { // fields were defined - preset[prop].forEach(function(fieldID) { - var match = fieldID.match(/\{(.*)\}/); - if (match !== null) { // presetID wrapped in braces {} - var inheritIDs = inheritedFieldIDs(match[1], prop); - if (inheritIDs !== null) { - fieldIDs = fieldIDs.concat(inheritIDs); - } else { - /* eslint-disable no-console */ - console.log('Cannot resolve presetID ' + match[0] + - ' found in ' + preset.id + ' ' + prop); - /* eslint-enable no-console */ - } - } else { - fieldIDs.push(fieldID); // no braces - just a normal field - } - }); - - } else { // no fields defined, so use the parent's if possible - fieldIDs = inheritedFieldIDs(preset.parentPresetID(), prop); - } - // resolve duplicate fields - fieldIDs = utilArrayUniq(fieldIDs); - - // update this preset with the results - preset[prop] = fieldIDs; - - // update the raw object to allow for multiple levels of inheritance - rawPresets[preset.id][prop] = fieldIDs; + } else { + fieldIDs.push(fieldID); // no braces - just a normal field + } }); + + } else { // no fields defined, so use the parent's if possible + fieldIDs = inheritedFieldIDs(_this.parentPresetID(), prop); + } + fieldIDs = utilArrayUniq(fieldIDs); + preset[prop] = fieldIDs; + rawPresets[_this.id][prop] = fieldIDs; + }); + } + + if (rawPresets) { + resolveFieldInheritance(); + } + + _this.fields = (_this.fields || []).map(getFields); + _this.moreFields = (_this.moreFields || []).map(getFields); + _this.geometry = (_this.geometry || []); + + function getFields(f) { + return fields[f]; + } + + + _this.matchGeometry = (geom) => _this.geometry.indexOf(geom) >= 0; + + _this.matchAllGeometry = (geometries) => { + return geometries.every(geom => _this.geometry.indexOf(geom) >= 0); + }; + + _this.originalScore = _this.matchScore || 1; + + _this.matchScore = (entityTags) => { + const tags = _this.tags; + let seen = {}; + let score = 0; + + // match on tags + for (let k in tags) { + seen[k] = true; + if (entityTags[k] === tags[k]) { + score += _this.originalScore; + } else if (tags[k] === '*' && k in entityTags) { + score += _this.originalScore / 2; + } else { + return -1; + } } - if (rawPresets) { - resolveFieldInheritance(); + // boost score for additional matches in addTags - #6802 + const addTags = _this.addTags; + for (let k in addTags) { + if (!seen[k] && entityTags[k] === addTags[k]) { + score += _this.originalScore; + } } - preset.fields = (preset.fields || []).map(getFields); - preset.moreFields = (preset.moreFields || []).map(getFields); - preset.geometry = (preset.geometry || []); + return score; + }; - addable = addable || false; - function getFields(f) { - return fields[f]; + let _textCache = {}; + _this.t = (scope, options) => { + const textID = `presets.presets.${presetID}.${scope}`; + if (_textCache[textID]) return _textCache[textID]; + return _textCache[textID] = t(textID, options); + }; + + + _this.originalName = _this.name || ''; + + + _this.name = () => { + if (_this.suggestion) { + let path = presetID.split('/'); + path.pop(); // remove brand name + // NOTE: insert an en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc) + return _this.originalName + ' – ' + t('presets.presets.' + path.join('/') + '.name'); + } + return _this.t('name', { 'default': _this.originalName }); + }; + + + _this.originalTerms = (_this.terms || []).join(); + + _this.terms = () => _this.t('terms', { 'default': _this.originalTerms }) + .toLowerCase().trim().split(/\s*,+\s*/); + + _this.isFallback = () => { + const tagCount = Object.keys(_this.tags).length; + return tagCount === 0 || (tagCount === 1 && _this.tags.hasOwnProperty('area')); + }; + + + addable = addable || false; + + _this.addable = function(val) { + if (!arguments.length) return addable; + addable = val; + return addable; + }; + + + const _reference = _this.reference || {}; + _this.reference = (geom) => { + // Lookup documentation on Wikidata... + const qid = _this.tags.wikidata || _this.tags['brand:wikidata'] || _this.tags['operator:wikidata']; + if (qid) { + return { qid: qid }; } + // Lookup documentation on OSM Wikibase... + let key = _reference.key || Object.keys(utilObjectOmit(_this.tags, 'name'))[0]; + let value = _reference.value || _this.tags[key]; - preset.matchGeometry = function(geometry) { - return preset.geometry.indexOf(geometry) >= 0; - }; + if (geom === 'relation' && key === 'type') { + if (value in _this.tags) { + key = value; + value = _this.tags[key]; + } else { + return { rtype: value }; + } + } + + if (value === '*') { + return { key: key }; + } else { + return { key: key, value: value }; + } + }; - preset.matchAllGeometry = function(geometries) { - return geometries.every(function(geometry) { - return preset.geometry.indexOf(geometry) >= 0; - }); - }; + _this.removeTags = _this.removeTags || _this.addTags || _this.tags || {}; + + _this.unsetTags = (tags, geometry) => { + tags = utilObjectOmit(tags, Object.keys(_this.removeTags)); + + for (let f in _this.fields) { + const field = _this.fields[f]; + if (field.matchGeometry(geometry) && field.default === tags[field.key]) { + delete tags[field.key]; + } + } + + delete tags.area; + return tags; + }; - preset.originalScore = preset.matchScore || 1; + _this.addTags = _this.addTags || _this.tags || {}; + _this.setTags = (tags, geometry, skipFieldDefaults) => { + const addTags = _this.addTags; + tags = Object.assign({}, tags); // shallow copy - preset.matchScore = function(entityTags) { - var tags = preset.tags; - var seen = {}; - var score = 0; - var k; + for (let k in addTags) { + if (addTags[k] === '*') { + tags[k] = 'yes'; + } else { + tags[k] = addTags[k]; + } + } - // match on tags - for (k in tags) { - seen[k] = true; - if (entityTags[k] === tags[k]) { - score += preset.originalScore; - } else if (tags[k] === '*' && k in entityTags) { - score += preset.originalScore / 2; - } else { - return -1; + // Add area=yes if necessary. + // This is necessary if the geometry is already an area (e.g. user drew an area) AND any of: + // 1. chosen preset could be either an area or a line (`barrier=city_wall`) + // 2. chosen preset doesn't have a key in osmAreaKeys (`railway=station`) + if (!addTags.hasOwnProperty('area')) { + delete tags.area; + if (geometry === 'area') { + let needsAreaTag = true; + if (_this.geometry.indexOf('line') === -1) { + for (let k in addTags) { + if (k in osmAreaKeys) { + needsAreaTag = false; + break; } + } } - - // boost score for additional matches in addTags - #6802 - var addTags = preset.addTags; - for (k in addTags) { - if (!seen[k] && entityTags[k] === addTags[k]) { - score += preset.originalScore; - } + if (needsAreaTag) { + tags.area = 'yes'; } - - return score; - }; - - - var _textCache = {}; - - preset.t = function(scope, options) { - var textID = 'presets.presets.' + id + '.' + scope; - - if (_textCache[textID]) return _textCache[textID]; - - var text = t(textID, options); - _textCache[textID] = text; - return text; - }; - - - preset.originalName = preset.name || ''; - - - preset.name = function() { - if (preset.suggestion) { - var path = id.split('/'); - path.pop(); // remove brand name - // NOTE: insert an en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc) - return preset.originalName + ' – ' + t('presets.presets.' + path.join('/') + '.name'); + } + } + if (geometry && !skipFieldDefaults) { + for (let f in _this.fields) { + const field = _this.fields[f]; + if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) { + tags[field.key] = field.default; } - return preset.t('name', { 'default': preset.originalName }); - }; + } + } + + return tags; + }; - preset.originalTerms = (preset.terms || []).join(); - - - preset.terms = function() { - return preset.t('terms', { 'default': preset.originalTerms }).toLowerCase().trim().split(/\s*,+\s*/); - }; - - - preset.isFallback = function() { - var tagCount = Object.keys(preset.tags).length; - return tagCount === 0 || (tagCount === 1 && preset.tags.hasOwnProperty('area')); - }; - - preset.addable = function(val) { - if (!arguments.length) return addable; - addable = val; - return addable; - }; - - - var reference = preset.reference || {}; - preset.reference = function(geometry) { - // Lookup documentation on Wikidata... - var qid = preset.tags.wikidata || preset.tags['brand:wikidata'] || preset.tags['operator:wikidata']; - if (qid) { - return { qid: qid }; - } - - // Lookup documentation on OSM Wikibase... - var key = reference.key || Object.keys(utilObjectOmit(preset.tags, 'name'))[0]; - var value = reference.value || preset.tags[key]; - - if (geometry === 'relation' && key === 'type') { - if (value in preset.tags) { - key = value; - value = preset.tags[key]; - } else { - return { rtype: value }; - } - } - - if (value === '*') { - return { key: key }; - } else { - return { key: key, value: value }; - } - }; - - - preset.removeTags = preset.removeTags || preset.addTags || preset.tags || {}; - preset.unsetTags = function(tags, geometry) { - tags = utilObjectOmit(tags, Object.keys(preset.removeTags)); - - for (var f in preset.fields) { - var field = preset.fields[f]; - if (field.matchGeometry(geometry) && field.default === tags[field.key]) { - delete tags[field.key]; - } - } - - delete tags.area; - return tags; - }; - - - preset.addTags = preset.addTags || preset.tags || {}; - preset.setTags = function(tags, geometry, skipFieldDefaults) { - var addTags = preset.addTags; - var k; - - tags = Object.assign({}, tags); // shallow copy - - for (k in addTags) { - if (addTags[k] === '*') { - tags[k] = 'yes'; - } else { - tags[k] = addTags[k]; - } - } - - // Add area=yes if necessary. - // This is necessary if the geometry is already an area (e.g. user drew an area) AND any of: - // 1. chosen preset could be either an area or a line (`barrier=city_wall`) - // 2. chosen preset doesn't have a key in osmAreaKeys (`railway=station`) - if (!addTags.hasOwnProperty('area')) { - delete tags.area; - if (geometry === 'area') { - var needsAreaTag = true; - if (preset.geometry.indexOf('line') === -1) { - for (k in addTags) { - if (k in osmAreaKeys) { - needsAreaTag = false; - break; - } - } - } - if (needsAreaTag) { - tags.area = 'yes'; - } - } - } - if (geometry && !skipFieldDefaults) { - for (var f in preset.fields) { - var field = preset.fields[f]; - if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) { - tags[field.key] = field.default; - } - } - } - - return tags; - }; - - - return preset; + return _this; } diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index f66d919b7..074fc6385 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -22,8 +22,9 @@ export function validationMismatchedGeometry(context) { return null; } - if (context.presets().matchTags(tagSuggestingArea, 'line') === - context.presets().matchTags(tagSuggestingArea, 'area')) { + var asLine = context.presets().matchTags(tagSuggestingArea, 'line'); + var asArea = context.presets().matchTags(tagSuggestingArea, 'area'); + if (asLine && asArea && (asLine === asArea)) { // these tags also allow lines and making this an area wouldn't matter return null; } @@ -31,6 +32,7 @@ export function validationMismatchedGeometry(context) { return tagSuggestingArea; } + function makeConnectEndpointsFixOnClick(way, graph) { // must have at least three nodes to close this automatically if (way.nodes.length < 3) return null; diff --git a/modules/validations/outdated_tags.js b/modules/validations/outdated_tags.js index 0e5fa3678..f97d730b1 100644 --- a/modules/validations/outdated_tags.js +++ b/modules/validations/outdated_tags.js @@ -56,6 +56,7 @@ export function validationOutdatedTags(context) { const oldTags = Object.assign({}, entity.tags); // shallow copy let preset = context.presets().match(entity, graph); let subtype = 'deprecated_tags'; + if (!preset) return []; // upgrade preset.. if (preset.replacement) { diff --git a/scripts/build_data.js b/scripts/build_data.js index 1dfbee72f..46fc3568f 100644 --- a/scripts/build_data.js +++ b/scripts/build_data.js @@ -106,7 +106,7 @@ function buildData() { // Save individual data files let tasks = [ - writeFileProm('data/presets/categories.json', prettyStringify({ categories: categories }) ), + writeFileProm('data/presets/categories.json', prettyStringify(categories) ), writeFileProm('data/presets/fields.json', prettyStringify({ fields: fields }, { maxLength: 9999 }) ), writeFileProm('data/presets/presets.json', prettyStringify({ presets: presets }, { maxLength: 9999 }) ), writeFileProm('data/presets.yaml', translationsToYAML(translations) ), @@ -115,9 +115,10 @@ function buildData() { writeEnJson(tstrings), writeFaIcons(faIcons), writeTnpIcons(tnpIcons), - minifyJSON('data/presets/categories.json', 'dist/data/categories.min.json'), - minifyJSON('data/presets/fields.json', 'dist/data/fields.min.json'), - minifyJSON('data/presets/presets.json', 'dist/data/presets.min.json'), + minifyJSON('data/presets/categories.json', 'dist/data/preset_categories.min.json'), + minifyJSON('data/presets/defaults.json', 'dist/data/preset_defaults.min.json'), + minifyJSON('data/presets/fields.json', 'dist/data/preset_fields.min.json'), + minifyJSON('data/presets/presets.json', 'dist/data/preset_presets.min.json'), minifyJSON('data/address_formats.json', 'dist/data/address_formats.min.json'), minifyJSON('data/deprecated.json', 'dist/data/deprecated.min.json'), minifyJSON('data/discarded.json', 'dist/data/discarded.min.json'), @@ -727,8 +728,8 @@ function validatePresetFields(presets, fields) { } function validateDefaults(defaults, categories, presets) { - Object.keys(defaults.defaults).forEach(name => { - let members = defaults.defaults[name]; + Object.keys(defaults).forEach(name => { + const members = defaults[name]; members.forEach(id => { if (!presets[id] && !categories[id]) { console.error(`Unknown category or preset: ${id} in default ${name}`); diff --git a/test/spec/osm/way.js b/test/spec/osm/way.js index c0789f192..13fc07429 100644 --- a/test/spec/osm/way.js +++ b/test/spec/osm/way.js @@ -1,4 +1,16 @@ describe('iD.osmWay', function() { + var _savedAreaKeys; + + before(function() { + _savedAreaKeys = iD.osmAreaKeys; + iD.osmSetAreaKeys({ building: {} }); + }); + + after(function() { + iD.osmSetAreaKeys(_savedAreaKeys); + }); + + if (iD.debug) { it('freezes nodes', function () { expect(Object.isFrozen(iD.osmWay().nodes)).to.be.true; @@ -405,10 +417,6 @@ describe('iD.osmWay', function() { }); describe('#isArea', function() { - before(function() { - iD.coreContext().init(); - }); - it('returns false when the way has no tags', function() { expect(iD.osmWay().isArea()).to.equal(false); }); @@ -421,7 +429,7 @@ describe('iD.osmWay', function() { expect(iD.osmWay({nodes: ['n1', 'n1']}).isArea()).to.equal(false); }); - it('returns true if the way is closed and has a key in iD.areaKeys', function() { + it('returns true if the way is closed and has a key in iD.osmAreaKeys', function() { expect(iD.osmWay({nodes: ['n1', 'n1'], tags: {building: 'yes'}}).isArea()).to.equal(true); }); @@ -435,7 +443,7 @@ describe('iD.osmWay', function() { expect(iD.osmWay({nodes: ['n1', 'n1'], tags: { railway: 'wash' }}).isArea(), 'railway=wash').to.equal(true); }); - it('returns false if the way is closed and has no keys in iD.areaKeys', function() { + it('returns false if the way is closed and has no keys in iD.osmAreaKeys', function() { expect(iD.osmWay({nodes: ['n1', 'n1'], tags: {a: 'b'}}).isArea()).to.equal(false); }); diff --git a/test/spec/presets/index.js b/test/spec/presets/index.js index 0db203444..d78dd6765 100644 --- a/test/spec/presets/index.js +++ b/test/spec/presets/index.js @@ -1,132 +1,162 @@ describe('iD.presetIndex', function () { - var savedPresets, savedAreaKeys, server; + var _savedPresets, _savedAreaKeys; - before(function () { - savedPresets = iD.data.presets; - savedAreaKeys = iD.areaKeys; + before(function() { + _savedPresets = iD.data.preset_presets; + _savedAreaKeys = iD.osmAreaKeys; }); - after(function () { - iD.data.presets = savedPresets; - iD.setAreaKeys(savedAreaKeys); + after(function() { + iD.data.preset_presets = _savedPresets; + iD.osmSetAreaKeys(_savedAreaKeys); }); + describe('#match', function () { var testPresets = { - presets: { - point: { tags: {}, geometry: ['point'] }, - line: { tags: {}, geometry: ['line'] }, - vertex: { tags: {}, geometry: ['vertex'] }, - residential: { tags: { highway: 'residential' }, geometry: ['line'] }, - park: { tags: { leisure: 'park' }, geometry: ['point', 'area'] } - } + point: { tags: {}, geometry: ['point'] }, + line: { tags: {}, geometry: ['line'] }, + vertex: { tags: {}, geometry: ['vertex'] }, + residential: { tags: { highway: 'residential' }, geometry: ['line'] }, + park: { tags: { leisure: 'park' }, geometry: ['point', 'area'] } }; - it('returns a collection containing presets matching a geometry and tags', function () { - iD.data.presets = testPresets; + it('returns a collection containing presets matching a geometry and tags', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var way = iD.osmWay({ tags: { highway: 'residential' } }); var graph = iD.coreGraph([way]); - - expect(presets.match(way, graph).id).to.eql('residential'); + window.setTimeout(function() { + expect(presets.match(way, graph).id).to.eql('residential'); + done(); + }, 20); }); - it('returns the appropriate fallback preset when no tags match', function () { - iD.data.presets = testPresets; + it('returns the appropriate fallback preset when no tags match', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var point = iD.osmNode(); var line = iD.osmWay({ tags: { foo: 'bar' } }); var graph = iD.coreGraph([point, line]); - expect(presets.match(point, graph).id).to.eql('point'); - expect(presets.match(line, graph).id).to.eql('line'); + window.setTimeout(function() { + expect(presets.match(point, graph).id).to.eql('point'); + expect(presets.match(line, graph).id).to.eql('line'); + done(); + }, 20); }); - it('matches vertices on a line as points', function () { - iD.data.presets = testPresets; + it('matches vertices on a line as points', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var point = iD.osmNode({ tags: { leisure: 'park' } }); var line = iD.osmWay({ nodes: [point.id], tags: { 'highway': 'residential' } }); var graph = iD.coreGraph([point, line]); - expect(presets.match(point, graph).id).to.eql('point'); + window.setTimeout(function() { + expect(presets.match(point, graph).id).to.eql('point'); + done(); + }, 20); }); - it('matches vertices on an addr:interpolation line as points', function () { - iD.data.presets = testPresets; + it('matches vertices on an addr:interpolation line as points', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var point = iD.osmNode({ tags: { leisure: 'park' } }); var line = iD.osmWay({ nodes: [point.id], tags: { 'addr:interpolation': 'even' } }); var graph = iD.coreGraph([point, line]); - expect(presets.match(point, graph).id).to.eql('park'); + window.setTimeout(function() { + expect(presets.match(point, graph).id).to.eql('park'); + done(); + }, 20); }); }); describe('#areaKeys', function () { var testPresets = { - presets: { - 'amenity/fuel/shell': { tags: { 'amenity': 'fuel' }, geometry: ['point', 'area'], suggestion: true }, - 'highway/foo': { tags: { 'highway': 'foo' }, geometry: ['area'] }, - 'leisure/track': { tags: { 'leisure': 'track' }, geometry: ['line', 'area'] }, - 'natural': { tags: { 'natural': '*' }, geometry: ['point', 'vertex', 'area'] }, - 'natural/peak': { tags: { 'natural': 'peak' }, geometry: ['point', 'vertex'] }, - 'natural/tree_row': { tags: { 'natural': 'tree_row' }, geometry: ['line'] }, - 'natural/wood': { tags: { 'natural': 'wood' }, geometry: ['point', 'area'] } - } + 'amenity/fuel/shell': { tags: { 'amenity': 'fuel' }, geometry: ['point', 'area'], suggestion: true }, + 'highway/foo': { tags: { 'highway': 'foo' }, geometry: ['area'] }, + 'leisure/track': { tags: { 'leisure': 'track' }, geometry: ['line', 'area'] }, + 'natural': { tags: { 'natural': '*' }, geometry: ['point', 'vertex', 'area'] }, + 'natural/peak': { tags: { 'natural': 'peak' }, geometry: ['point', 'vertex'] }, + 'natural/tree_row': { tags: { 'natural': 'tree_row' }, geometry: ['line'] }, + 'natural/wood': { tags: { 'natural': 'wood' }, geometry: ['point', 'area'] } }; - it('whitelists keys for presets with area geometry', function () { - iD.data.presets = testPresets; + it('includes keys for presets with area geometry', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys()).to.include.keys('natural'); + window.setTimeout(function() { + expect(presets.areaKeys()).to.include.keys('natural'); + done(); + }, 20); }); - it('blacklists key-values for presets with a line geometry', function () { - iD.data.presets = testPresets; + it('discards key-values for presets with a line geometry', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys().natural).to.include.keys('tree_row'); - expect(presets.areaKeys().natural.tree_row).to.be.true; + window.setTimeout(function() { + expect(presets.areaKeys().natural).to.include.keys('tree_row'); + expect(presets.areaKeys().natural.tree_row).to.be.true; + done(); + }, 20); }); - it('blacklists key-values for presets with both area and line geometry', function () { - iD.data.presets = testPresets; + it('discards key-values for presets with both area and line geometry', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys().leisure).to.include.keys('track'); + window.setTimeout(function() { + expect(presets.areaKeys().leisure).to.include.keys('track'); + done(); + }, 20); }); - it('does not blacklist key-values for presets with neither area nor line geometry', function () { - iD.data.presets = testPresets; + it('does not discard key-values for presets with neither area nor line geometry', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys().natural).not.to.include.keys('peak'); + window.setTimeout(function() { + expect(presets.areaKeys().natural).not.to.include.keys('peak'); + done(); + }, 20); }); - it('does not blacklist generic \'*\' key-values', function () { - iD.data.presets = testPresets; + it('does not discard generic \'*\' key-values', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys().natural).not.to.include.keys('natural'); + window.setTimeout(function() { + expect(presets.areaKeys().natural).not.to.include.keys('natural'); + done(); + }, 20); }); - it('ignores keys like \'highway\' that are assumed to be lines', function () { - iD.data.presets = testPresets; + it('ignores keys like \'highway\' that are assumed to be lines', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys()).not.to.include.keys('highway'); + window.setTimeout(function() { + expect(presets.areaKeys()).not.to.include.keys('highway'); + done(); + }, 20); }); - it('ignores suggestion presets', function () { - iD.data.presets = testPresets; + it('ignores suggestion presets', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); - expect(presets.areaKeys()).not.to.include.keys('amenity'); + window.setTimeout(function() { + expect(presets.areaKeys()).not.to.include.keys('amenity'); + done(); + }, 20); }); }); - describe('#build', function () { + + describe.skip('#build', function () { it('builds presets from provided', function () { var surfShop = iD.osmNode({ tags: { amenity: 'shop', 'shop:type': 'surf' } }); var graph = iD.coreGraph([surfShop]); var presets = iD.coreContext().init().presets(); - var morePresets = { + var presetData = { presets: { 'amenity/shop/surf': { tags: { amenity: 'shop', 'shop:type': 'surf' }, @@ -136,7 +166,7 @@ describe('iD.presetIndex', function () { }; expect(presets.match(surfShop, graph)).to.eql(undefined); // no surfshop preset yet... - presets.build(morePresets, true); + presets.build(presetData, true); expect(presets.match(surfShop, graph).addTags).to.eql({ amenity: 'shop', 'shop:type': 'surf' }); }); @@ -146,7 +176,7 @@ describe('iD.presetIndex', function () { var entities = [surfShop, firstStreetJetty]; var graph = iD.coreGraph(entities); var presets = iD.coreContext().init().presets(); - var morePresets = { + var presetData = { presets: { 'amenity/shop/surf': { tags: { amenity: 'shop', 'shop:type': 'surf' }, @@ -159,7 +189,7 @@ describe('iD.presetIndex', function () { } }; - presets.build(morePresets, false); + presets.build(presetData, false); entities.forEach(function (entity) { var preset = presets.match(entity, graph); expect(preset.addable()).to.be.false; @@ -167,64 +197,76 @@ describe('iD.presetIndex', function () { }); }); + describe('expected matches', function () { var testPresets = { - presets: { - area: { name: 'Area', tags: {}, geometry: ['area'] }, - line: { name: 'Line', tags: {}, geometry: ['line'] }, - point: { name: 'Point', tags: {}, geometry: ['point'] }, - vertex: { name: 'Vertex', tags: {}, geometry: ['vertex'] }, - relation: { name: 'Relation', tags: {}, geometry: ['relation'] }, - building: { name: 'Building', tags: { building: 'yes' }, geometry: ['area'] }, - 'type/multipolygon': { - name: 'Multipolygon', - geometry: ['area', 'relation'], - tags: { 'type': 'multipolygon' }, - searchable: false, - matchScore: 0.1 - }, - address: { - name: 'Address', - geometry: ['point', 'vertex', 'area'], - tags: { 'addr:*': '*' }, - matchScore: 0.15 - }, - 'highway/pedestrian_area': { - name: 'Pedestrian Area', - geometry: ['area'], - tags: { highway: 'pedestrian', area: 'yes' } - } + area: { name: 'Area', tags: {}, geometry: ['area'] }, + line: { name: 'Line', tags: {}, geometry: ['line'] }, + point: { name: 'Point', tags: {}, geometry: ['point'] }, + vertex: { name: 'Vertex', tags: {}, geometry: ['vertex'] }, + relation: { name: 'Relation', tags: {}, geometry: ['relation'] }, + building: { name: 'Building', tags: { building: 'yes' }, geometry: ['area'] }, + 'type/multipolygon': { + name: 'Multipolygon', + geometry: ['area', 'relation'], + tags: { 'type': 'multipolygon' }, + searchable: false, + matchScore: 0.1 + }, + address: { + name: 'Address', + geometry: ['point', 'vertex', 'area'], + tags: { 'addr:*': '*' }, + matchScore: 0.15 + }, + 'highway/pedestrian_area': { + name: 'Pedestrian Area', + geometry: ['area'], + tags: { highway: 'pedestrian', area: 'yes' } } }; - it('prefers building to multipolygon', function () { - iD.data.presets = testPresets; + it('prefers building to multipolygon', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var relation = iD.osmRelation({ tags: { type: 'multipolygon', building: 'yes' } }); var graph = iD.coreGraph([relation]); - expect(presets.match(relation, graph).id).to.eql('building'); + window.setTimeout(function() { + var match = presets.match(relation, graph); + expect(match.id).to.eql('building'); + done(); + }, 20); }); - it('prefers building to address', function () { - iD.data.presets = testPresets; + it('prefers building to address', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var way = iD.osmWay({ tags: { area: 'yes', building: 'yes', 'addr:housenumber': '1234' } }); var graph = iD.coreGraph([way]); - expect(presets.match(way, graph).id).to.eql('building'); + window.setTimeout(function() { + var match = presets.match(way, graph); + expect(match.id).to.eql('building'); + done(); + }, 20); }); - it('prefers pedestrian to area', function () { - iD.data.presets = testPresets; + it('prefers pedestrian to area', function (done) { + iD.data.preset_presets = testPresets; var presets = iD.coreContext().init().presets(); var way = iD.osmWay({ tags: { area: 'yes', highway: 'pedestrian' } }); var graph = iD.coreGraph([way]); - expect(presets.match(way, graph).id).to.eql('highway/pedestrian_area'); + window.setTimeout(function() { + var match = presets.match(way, graph); + expect(match.id).to.eql('highway/pedestrian_area'); + done(); + }, 20); }); }); - describe('#fromExternal', function () { - var morePresets = { + describe.skip('#fromExternal', function () { + var _server; + var presetData = { presets: { '8bc64d6d': { 'name': 'Surf Shop', @@ -245,11 +287,11 @@ describe('iD.presetIndex', function () { }; beforeEach(function () { - server = window.fakeFetch().create(); + _server = window.fakeFetch().create(); }); afterEach(function () { - server.restore(); + _server.restore(); }); it('builds presets w/external sources set to addable', function () { @@ -268,10 +310,10 @@ describe('iD.presetIndex', function () { expect(externalPresets.match(surfShop, graph).id).to.eql('8bc64d6d'); }); - server.respondWith('GET', /fake\.json/, - [200, { 'Content-Type': 'application/json' }, JSON.stringify(morePresets)] + _server.respondWith('GET', /fake\.json/, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(presetData)] ); - server.respond(); + _server.respond(); }); it('makes only the external presets initially addable', function () { @@ -285,7 +327,7 @@ describe('iD.presetIndex', function () { return presets; }, []); - var morePresetKeys = Object.keys(morePresets.presets); + var morePresetKeys = Object.keys(presetData.presets); expect(morePresetKeys.length).to.eql(external.length); morePresetKeys.forEach(function(presetID) { @@ -293,10 +335,10 @@ describe('iD.presetIndex', function () { }); }); - server.respondWith('GET', /fake\.json/, - [200, { 'Content-Type': 'application/json' }, JSON.stringify(morePresets)] + _server.respondWith('GET', /fake\.json/, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(presetData)] ); - server.respond(); + _server.respond(); }); }); diff --git a/test/spec/presets/preset.js b/test/spec/presets/preset.js index 7484f8f7e..c16054b1b 100644 --- a/test/spec/presets/preset.js +++ b/test/spec/presets/preset.js @@ -97,15 +97,15 @@ describe('iD.presetPreset', function() { }); describe('#setTags', function() { - var savedAreaKeys; + var _savedAreaKeys; before(function () { - savedAreaKeys = iD.areaKeys; - iD.setAreaKeys({ building: {}, natural: {} }); + _savedAreaKeys = iD.osmAreaKeys; + iD.osmSetAreaKeys({ building: {}, natural: {} }); }); after(function () { - iD.setAreaKeys(savedAreaKeys); + iD.osmSetAreaKeys(_savedAreaKeys); }); it('adds match tags', function() { diff --git a/test/spec/services/maprules.js b/test/spec/services/maprules.js index a1b8ac95e..60362c022 100644 --- a/test/spec/services/maprules.js +++ b/test/spec/services/maprules.js @@ -1,9 +1,9 @@ describe('maprules', function() { - var _ruleChecks, savedAreaKeys, validationRules; + var _ruleChecks, _savedAreaKeys, validationRules; before(function() { - savedAreaKeys = iD.areaKeys; - iD.setAreaKeys({ building: {}, amenity: {} }); + _savedAreaKeys = iD.osmAreaKeys; + iD.osmSetAreaKeys({ building: {}, amenity: {} }); iD.services.maprules = iD.serviceMapRules; iD.serviceMapRules.init(); @@ -11,7 +11,7 @@ describe('maprules', function() { }); after(function() { - iD.setAreaKeys(savedAreaKeys); + iD.osmSetAreaKeys(_savedAreaKeys); delete iD.services.maprules; }); diff --git a/test/spec/spec_helpers.js b/test/spec/spec_helpers.js index e11291ac3..c658c21e2 100644 --- a/test/spec/spec_helpers.js +++ b/test/spec/spec_helpers.js @@ -7,19 +7,31 @@ iD.data.imagery = []; for (var k in iD.services) { delete iD.services[k]; } // run with a minimal set of presets for speed -iD.data.presets = { - presets: { - area: { name: 'Area', tags: {}, geometry: ['area'] }, - line: { name: 'Line', tags: {}, geometry: ['line'] }, - point: { name: 'Point', tags: {}, geometry: ['point'] }, - vertex: { name: 'Vertex', tags: {}, geometry: ['vertex'] }, - relation: { name: 'Relation', tags: {}, geometry: ['relation'] }, - // for tests related to areaKeys: - building: { name: 'Building', tags: { building: 'yes' }, geometry: ['point', 'area'] }, - man_made: { name: 'Man Made', tags: { man_made: '*' }, geometry: ['vertex', 'point', 'line', 'area'] } - } +// iD.data.presets = { +// presets: { +// area: { name: 'Area', tags: {}, geometry: ['area'] }, +// line: { name: 'Line', tags: {}, geometry: ['line'] }, +// point: { name: 'Point', tags: {}, geometry: ['point'] }, +// vertex: { name: 'Vertex', tags: {}, geometry: ['vertex'] }, +// relation: { name: 'Relation', tags: {}, geometry: ['relation'] }, +// // for tests related to areaKeys: +// building: { name: 'Building', tags: { building: 'yes' }, geometry: ['point', 'area'] }, +// man_made: { name: 'Man Made', tags: { man_made: '*' }, geometry: ['vertex', 'point', 'line', 'area'] } +// } +// }; + +iD.data.preset_categories = {}; +iD.data.preset_defaults = {}; +iD.data.preset_fields = {}; +iD.data.preset_presets = { + area: { name: 'Area', tags: {}, geometry: ['area'] }, + line: { name: 'Line', tags: {}, geometry: ['line'] }, + point: { name: 'Point', tags: {}, geometry: ['point'] }, + vertex: { name: 'Vertex', tags: {}, geometry: ['vertex'] }, + relation: { name: 'Relation', tags: {}, geometry: ['relation'] } }; + // creating `coreContext` creates validators and some of the validators try loading these iD.data.deprecated = []; iD.data.nsi_brands = []; diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index a08fbf5d2..d98a4d619 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,7 +1,8 @@ describe('iD.svgAreas', function () { - var context, surface, savedAreaKeys; + var context, _surface, _savedAreaKeys; var all = function() { return true; }; var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) .scale(iD.geoZoomToScale(17)) @@ -13,14 +14,14 @@ describe('iD.svgAreas', function () { d3.select(document.createElement('div')) .attr('id', 'map') .call(context.map().centerZoom([0, 0], 17)); - surface = context.surface(); + _surface = context.surface(); - savedAreaKeys = iD.areaKeys; - iD.setAreaKeys({ building: {}, landuse: {}, natural: {} }); + _savedAreaKeys = iD.osmAreaKeys; + iD.osmSetAreaKeys({ building: {}, landuse: {}, natural: {} }); }); afterEach(function () { - iD.setAreaKeys(savedAreaKeys); + iD.osmSetAreaKeys(_savedAreaKeys); }); @@ -30,13 +31,13 @@ describe('iD.svgAreas', function () { iD.osmNode({id: 'b', loc: [1, 0]}), iD.osmNode({id: 'c', loc: [1, 1]}), iD.osmNode({id: 'd', loc: [0, 1]}), - iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + iD.osmWay({id: 'w', tags: {area: 'yes', building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) ]); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); - expect(surface.select('path.way').classed('way')).to.be.true; - expect(surface.select('path.area').classed('area')).to.be.true; + expect(_surface.select('path.way').classed('way')).to.be.true; + expect(_surface.select('path.area').classed('area')).to.be.true; }); it('adds tag classes', function () { @@ -45,13 +46,13 @@ describe('iD.svgAreas', function () { iD.osmNode({id: 'b', loc: [1, 0]}), iD.osmNode({id: 'c', loc: [1, 1]}), iD.osmNode({id: 'd', loc: [0, 1]}), - iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + iD.osmWay({id: 'w', tags: {area: 'yes', building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) ]); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); - expect(surface.select('.area').classed('tag-building')).to.be.true; - expect(surface.select('.area').classed('tag-building-yes')).to.be.true; + expect(_surface.select('.area').classed('tag-building')).to.be.true; + expect(_surface.select('.area').classed('tag-building-yes')).to.be.true; }); it('handles deletion of a way and a member vertex (#1903)', function () { @@ -64,11 +65,11 @@ describe('iD.svgAreas', function () { iD.osmWay({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) ]); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('x')], all); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('x')], all); graph = graph.remove(graph.entity('x')).remove(graph.entity('d')); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], all); - expect(surface.select('.area').size()).to.equal(1); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], all); + expect(_surface.select('.area').size()).to.equal(1); }); describe('z-indexing', function() { @@ -81,38 +82,38 @@ describe('iD.svgAreas', function () { iD.osmNode({id: 'f', loc: [ 0.0004, 0.0002]}), iD.osmNode({id: 'g', loc: [ 0.0004, -0.0002]}), iD.osmNode({id: 'h', loc: [-0.0004, -0.0002]}), - iD.osmWay({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), - iD.osmWay({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) + iD.osmWay({id: 's', tags: {area: 'yes', building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), + iD.osmWay({id: 'l', tags: {area: 'yes', landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) ]); it('stacks smaller areas above larger ones in a single render', function () { - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s'), graph.entity('l')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s'), graph.entity('l')], none); - expect(surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; - expect(surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; + expect(_surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; + expect(_surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; }); it('stacks smaller areas above larger ones in a single render (reverse)', function () { - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l'), graph.entity('s')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l'), graph.entity('s')], none); - expect(surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; - expect(surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; + expect(_surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; + expect(_surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; }); it('stacks smaller areas above larger ones in separate renders', function () { - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s')], none); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l')], none); - expect(surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; - expect(surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; + expect(_surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; + expect(_surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; }); it('stacks smaller areas above larger ones in separate renders (reverse)', function () { - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l')], none); - surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('l')], none); + _surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s')], none); - expect(surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; - expect(surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; + expect(_surface.select('.area:nth-child(1)').classed('tag-landuse-park')).to.be.true; + expect(_surface.select('.area:nth-child(2)').classed('tag-building-yes')).to.be.true; }); }); @@ -125,9 +126,9 @@ describe('iD.svgAreas', function () { var graph = iD.coreGraph([a, b, c, w, r]); var areas = [w, r]; - surface.call(iD.svgAreas(projection, context), graph, areas, none); + _surface.call(iD.svgAreas(projection, context), graph, areas, none); - expect(surface.select('.fill').classed('relation')).to.be.true; + expect(_surface.select('.fill').classed('relation')).to.be.true; }); it('renders no strokes for multipolygon areas', function () { @@ -139,9 +140,9 @@ describe('iD.svgAreas', function () { var graph = iD.coreGraph([a, b, c, w, r]); var areas = [w, r]; - surface.call(iD.svgAreas(projection, context), graph, areas, none); + _surface.call(iD.svgAreas(projection, context), graph, areas, none); - expect(surface.selectAll('.stroke').size()).to.equal(0); + expect(_surface.selectAll('.stroke').size()).to.equal(0); }); it('renders fill for a multipolygon with tags on the outer way', function() { @@ -152,11 +153,11 @@ describe('iD.svgAreas', function () { var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); var graph = iD.coreGraph([a, b, c, w, r]); - surface.call(iD.svgAreas(projection, context), graph, [w, r], none); + _surface.call(iD.svgAreas(projection, context), graph, [w, r], none); - expect(surface.selectAll('.way.fill').size()).to.equal(0); - expect(surface.selectAll('.relation.fill').size()).to.equal(1); - expect(surface.select('.relation.fill').classed('tag-natural-wood')).to.be.true; + expect(_surface.selectAll('.way.fill').size()).to.equal(0); + expect(_surface.selectAll('.relation.fill').size()).to.equal(1); + expect(_surface.select('.relation.fill').classed('tag-natural-wood')).to.be.true; }); it('renders no strokes for a multipolygon with tags on the outer way', function() { @@ -167,8 +168,8 @@ describe('iD.svgAreas', function () { var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); var graph = iD.coreGraph([a, b, c, w, r]); - surface.call(iD.svgAreas(projection, context), graph, [w, r], none); + _surface.call(iD.svgAreas(projection, context), graph, [w, r], none); - expect(surface.selectAll('.stroke').size()).to.equal(0); + expect(_surface.selectAll('.stroke').size()).to.equal(0); }); }); diff --git a/test/spec/validations/mismatched_geometry.js b/test/spec/validations/mismatched_geometry.js index b7279b522..16a907122 100644 --- a/test/spec/validations/mismatched_geometry.js +++ b/test/spec/validations/mismatched_geometry.js @@ -1,10 +1,16 @@ describe('iD.validations.mismatched_geometry', function () { - var context; + var context, _savedAreaKeys; beforeEach(function() { + _savedAreaKeys = iD.osmAreaKeys; context = iD.coreContext().init(); }); + afterEach(function() { + iD.osmSetAreaKeys(_savedAreaKeys); + }); + + function createPoint(tags) { var n1 = iD.osmNode({id: 'n-1', loc: [4,4], tags: tags}); context.perform( @@ -82,6 +88,7 @@ describe('iD.validations.mismatched_geometry', function () { }); it('flags open way with area tag', function() { + iD.osmSetAreaKeys({ building: {} }); createOpenWay({ building: 'yes' }); var issues = validate(); expect(issues).to.have.lengthOf(1);