WIP on external presets

- preset data is no longer bundled into iD.js
- some code pathways commented out re: external presets
- many changes so that tests can run without presets at start, or async
- still need to make sure fallbacks are always there (point, line, area, etc)
This commit is contained in:
Bryan Housel
2020-02-05 09:38:26 -05:00
parent a333a341ec
commit 0fe766d9a4
21 changed files with 1435 additions and 1435 deletions

View File

@@ -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 = {};

View File

@@ -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"
]
}
}

View File

@@ -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"
]
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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' &&

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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}`);

View File

@@ -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);
});

View File

@@ -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();
});
});

View File

@@ -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() {

View File

@@ -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;
});

View File

@@ -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 = [];

View File

@@ -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);
});
});

View File

@@ -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);