Merge branch 'develop' into validation_queryparams_conflict_resolve

This commit is contained in:
Milos Brzakovic (E-Search)
2021-07-06 19:07:07 +02:00
81 changed files with 28606 additions and 17435 deletions
+2 -1
View File
@@ -4,7 +4,8 @@ export function actionChangePreset(entityID, oldPreset, newPreset, skipFieldDefa
var geometry = entity.geometry(graph);
var tags = entity.tags;
if (oldPreset) tags = oldPreset.unsetTags(tags, geometry);
// preserve tags that the new preset might care about, if any
if (oldPreset) tags = oldPreset.unsetTags(tags, geometry, newPreset && newPreset.addTags ? Object.keys(newPreset.addTags) : null);
if (newPreset) tags = newPreset.setTags(tags, geometry, skipFieldDefaults);
return graph.replace(entity.update({tags: tags}));
+4 -5
View File
@@ -1,10 +1,9 @@
import { geoCentroid as d3_geoCentroid } from 'd3-geo';
import geojsonRewind from '@mapbox/geojson-rewind';
import { geoPath as d3_geoPath } from 'd3-geo';
import { osmNode } from '../osm/node';
export function actionExtract(entityID) {
export function actionExtract(entityID, projection) {
var extractedNodeID;
@@ -47,8 +46,8 @@ export function actionExtract(entityID) {
var keysToRetain = ['area'];
var buildingKeysToRetain = ['architect', 'building', 'height', 'layer'];
// d3_geoCentroid is wrong for counterclockwise-wound polygons, so wind them clockwise
var extractedLoc = d3_geoCentroid(geojsonRewind(Object.assign({}, entity.asGeoJSON(graph)), true));
var extractedLoc = d3_geoPath(projection).centroid(entity.asGeoJSON(graph));
extractedLoc = extractedLoc && projection.invert(extractedLoc);
if (!extractedLoc || !isFinite(extractedLoc[0]) || !isFinite(extractedLoc[1])) {
extractedLoc = entity.extent(graph).center();
}
+7 -7
View File
@@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection';
import { t } from '../core/localizer';
import { fileFetcher as data } from './file_fetcher';
import { fileFetcher } from './file_fetcher';
import { localizer } from './localizer';
import { prefs } from './preferences';
import { coreHistory } from './history';
@@ -447,7 +447,7 @@ export function coreContext() {
context.assetPath = function(val) {
if (!arguments.length) return _assetPath;
_assetPath = val;
data.assetPath(val);
fileFetcher.assetPath(val);
return context;
};
@@ -455,7 +455,7 @@ export function coreContext() {
context.assetMap = function(val) {
if (!arguments.length) return _assetMap;
_assetMap = val;
data.assetMap(val);
fileFetcher.assetMap(val);
return context;
};
@@ -576,13 +576,13 @@ export function coreContext() {
// if the container isn't available, e.g. when testing, don't load the UI
if (!context.container().empty()) {
_ui.ensureLoaded().then(function() {
_photos.init();
});
_ui.ensureLoaded()
.then(() => {
_photos.init();
});
}
}
};
return context;
}
+5 -6
View File
@@ -1,4 +1,4 @@
import { json as d3_json } from 'd3-fetch';
import { utilFetchJson } from '../util/util';
let _mainFileFetcher = coreFileFetcher(); // singleton
@@ -19,10 +19,9 @@ export function coreFileFetcher() {
'keepRight': 'data/keepRight.min.json',
'languages': 'data/languages.min.json',
'locales': 'locales/index.min.json',
'nsi_brands': 'https://cdn.jsdelivr.net/npm/name-suggestion-index@4/dist/brands.min.json',
'nsi_filters': 'https://cdn.jsdelivr.net/npm/name-suggestion-index@4/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',
'oci_defaults': 'https://cdn.jsdelivr.net/npm/osm-community-index@4/dist/defaults.min.json',
'oci_features': 'https://cdn.jsdelivr.net/npm/osm-community-index@4/dist/featureCollection.min.json',
'oci_resources': 'https://cdn.jsdelivr.net/npm/osm-community-index@4/dist/resources.min.json',
'preset_categories': 'https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/preset_categories.min.json',
'preset_defaults': 'https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/preset_defaults.min.json',
'preset_fields': 'https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@3/dist/fields.min.json',
@@ -54,7 +53,7 @@ export function coreFileFetcher() {
let prom = _inflight[url];
if (!prom) {
_inflight[url] = prom = d3_json(url)
_inflight[url] = prom = utilFetchJson(url)
.then(result => {
delete _inflight[url];
if (!result) {
+1
View File
@@ -4,6 +4,7 @@ export { coreDifference } from './difference';
export { coreGraph } from './graph';
export { coreHistory } from './history';
export { coreLocalizer, t, localizer } from './localizer';
export { coreLocations, locationManager } from './locations';
export { prefs } from './preferences';
export { coreTree } from './tree';
export { coreUploader } from './uploader';
+24 -22
View File
@@ -94,9 +94,9 @@ export function coreLocalizer() {
};
let fileMap = fileFetcher.fileMap();
for (let id in localeDirs) {
let key = `locales_index_${id}`;
fileMap[key] = localeDirs[id] + '/index.min.json';
for (let scopeId in localeDirs) {
let key = `locales_index_${scopeId}`;
fileMap[key] = localeDirs[scopeId] + '/index.min.json';
filesToFetch.push(key);
}
@@ -126,9 +126,9 @@ export function coreLocalizer() {
});
// We only need to load locales up until we find one with full coverage
_localeCodes.slice(0, fullCoverageIndex + 1).forEach(function(code) {
let id = Object.keys(localeDirs)[i];
let dir = Object.values(localeDirs)[i];
if (index[code]) loadStringsPromises.push(localizer.loadLocale(code, id, dir));
let scopeId = Object.keys(localeDirs)[i];
let directory = Object.values(localeDirs)[i];
if (index[code]) loadStringsPromises.push(localizer.loadLocale(code, scopeId, directory));
});
});
@@ -187,25 +187,23 @@ export function coreLocalizer() {
/* Locales */
// Returns a Promise to load the strings for the requested locale
localizer.loadLocale = (requested, id, dir) => {
let locale = requested;
localizer.loadLocale = (locale, scopeId, directory) => {
// US English is the default
if (locale.toLowerCase() === 'en-us') locale = 'en';
if (_localeStrings[id] && _localeStrings[id][locale]) { // already loaded
if (_localeStrings[scopeId] && _localeStrings[scopeId][locale]) { // already loaded
return Promise.resolve(locale);
}
let fileMap = fileFetcher.fileMap();
const key = `locale_${id}_${locale}`;
fileMap[key] = `${dir}/${locale}.min.json`;
const key = `locale_${scopeId}_${locale}`;
fileMap[key] = `${directory}/${locale}.min.json`;
return fileFetcher.get(key)
.then(d => {
if (!_localeStrings[id]) _localeStrings[id] = {};
_localeStrings[id][locale] = d[locale];
if (!_localeStrings[scopeId]) _localeStrings[scopeId] = {};
_localeStrings[scopeId][locale] = d[locale];
return locale;
});
};
@@ -240,7 +238,7 @@ export function coreLocalizer() {
* @return {string?} localized string
*/
localizer.tInfo = function(origStringId, replacements, locale) {
let stringId = origStringId;
let stringId = origStringId.trim();
let scopeId = 'general';
@@ -289,13 +287,17 @@ export function coreLocalizer() {
if (typeof result === 'string') {
for (let key in replacements) {
let value = replacements[key];
if (typeof value === 'number' && value.toLocaleString) {
// format numbers for the locale
value = value.toLocaleString(locale, {
style: 'decimal',
useGrouping: true,
minimumFractionDigits: 0
});
if (typeof value === 'number') {
if (value.toLocaleString) {
// format numbers for the locale
value = value.toLocaleString(locale, {
style: 'decimal',
useGrouping: true,
minimumFractionDigits: 0
});
} else {
value = value.toString();
}
}
const token = `{${key}}`;
const regex = new RegExp(token, 'g');
+270
View File
@@ -0,0 +1,270 @@
import LocationConflation from '@ideditor/location-conflation';
import whichPolygon from 'which-polygon';
import calcArea from '@mapbox/geojson-area';
import { utilArrayChunk } from '../util';
let _mainLocations = coreLocations(); // singleton
export { _mainLocations as locationManager };
//
// `coreLocations` maintains an internal index of all the boundaries/geofences used by iD.
// It's used by presets, community index, background imagery, to know where in the world these things are valid.
// These geofences should be defined by `locationSet` objects:
//
// let locationSet = {
// include: [ Array of locations ],
// exclude: [ Array of locations ]
// };
//
// For more info see the location-conflation and country-coder projects, see:
// https://github.com/ideditor/location-conflation
// https://github.com/ideditor/country-coder
//
export function coreLocations() {
let _this = {};
let _resolvedFeatures = {}; // cache of *resolved* locationSet features
let _loco = new LocationConflation(); // instance of a location-conflation resolver
let _wp; // instance of a which-polygon index
// pre-resolve the worldwide locationSet
const world = { locationSet: { include: ['Q2'] } };
resolveLocationSet(world);
rebuildIndex();
let _queue = [];
let _deferred = new Set();
let _inProcess;
// Returns a Promise to process the queue
function processQueue() {
if (!_queue.length) return Promise.resolve();
// console.log(`queue length ${_queue.length}`);
const chunk = _queue.pop();
return new Promise(resolvePromise => {
const handle = window.requestIdleCallback(() => {
_deferred.delete(handle);
// const t0 = performance.now();
chunk.forEach(resolveLocationSet);
// const t1 = performance.now();
// console.log('chunk processed in ' + (t1 - t0) + ' ms');
resolvePromise();
});
_deferred.add(handle);
})
.then(() => processQueue());
}
// Pass an Object with a `locationSet` property,
// Performs the locationSet resolution, caches the result, and sets a `locationSetID` property on the object.
function resolveLocationSet(obj) {
if (obj.locationSetID) return; // work was done already
try {
let locationSet = obj.locationSet;
if (!locationSet) {
throw new Error('object missing locationSet property');
}
if (!locationSet.include) { // missing `include`, default to worldwide include
locationSet.include = ['Q2']; // https://github.com/openstreetmap/iD/pull/8305#discussion_r662344647
}
const resolved = _loco.resolveLocationSet(locationSet);
const locationSetID = resolved.id;
obj.locationSetID = locationSetID;
if (!resolved.feature.geometry.coordinates.length || !resolved.feature.properties.area) {
throw new Error(`locationSet ${locationSetID} resolves to an empty feature.`);
}
if (!_resolvedFeatures[locationSetID]) { // First time seeing this locationSet feature
let feature = JSON.parse(JSON.stringify(resolved.feature)); // deep clone
feature.id = locationSetID; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)
feature.properties.id = locationSetID;
_resolvedFeatures[locationSetID] = feature; // insert into cache
}
} catch (err) {
obj.locationSet = { include: ['Q2'] }; // default worldwide
obj.locationSetID = '+[Q2]';
}
}
// Rebuilds the whichPolygon index with whatever features have been resolved.
function rebuildIndex() {
_wp = whichPolygon({ features: Object.values(_resolvedFeatures) });
}
//
// `mergeCustomGeoJSON`
// Accepts an FeatureCollection-like object containing custom locations
// Each feature must have a filename-like `id`, for example: `something.geojson`
//
// {
// "type": "FeatureCollection"
// "features": [
// {
// "type": "Feature",
// "id": "philly_metro.geojson",
// "properties": { … },
// "geometry": { … }
// }
// ]
// }
//
_this.mergeCustomGeoJSON = (fc) => {
if (fc && fc.type === 'FeatureCollection' && Array.isArray(fc.features)) {
fc.features.forEach(feature => {
feature.properties = feature.properties || {};
let props = feature.properties;
// Get `id` from either `id` or `properties`
let id = feature.id || props.id;
if (!id || !/^\S+\.geojson$/i.test(id)) return;
// Ensure `id` exists and is lowercase
id = id.toLowerCase();
feature.id = id;
props.id = id;
// Ensure `area` property exists
if (!props.area) {
const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²
props.area = Number(area.toFixed(2));
}
_loco._cache[id] = feature;
});
}
};
//
// `mergeLocationSets`
// Accepts an Array of Objects containing `locationSet` properties.
// The locationSets will be resolved and indexed in the background.
// [
// { id: 'preset1', locationSet: {…} },
// { id: 'preset2', locationSet: {…} },
// { id: 'preset3', locationSet: {…} },
// …
// ]
// After resolving and indexing, the Objects will be decorated with a
// `locationSetID` property.
// [
// { id: 'preset1', locationSet: {…}, locationSetID: '+[Q2]' },
// { id: 'preset2', locationSet: {…}, locationSetID: '+[Q30]' },
// { id: 'preset3', locationSet: {…}, locationSetID: '+[Q2]' },
// …
// ]
//
// Returns a Promise fulfilled when the resolving/indexing has been completed
// This will take some seconds but happen in the background during browser idle time.
//
_this.mergeLocationSets = (objects) => {
if (!Array.isArray(objects)) return Promise.reject('nothing to do');
// Resolve all locationSets -> geojson, processing data in chunks
//
// Because this will happen during idle callbacks, we want to choose a chunk size
// that won't make the browser stutter too badly. LocationSets that are a simple
// country coder include will resolve instantly, but ones that involve complex
// include/exclude operations will take some milliseconds longer.
//
// Some discussion and performance results on these tickets:
// https://github.com/ideditor/location-conflation/issues/26
// https://github.com/osmlab/name-suggestion-index/issues/4784#issuecomment-742003434
_queue = _queue.concat(utilArrayChunk(objects, 200));
if (!_inProcess) {
_inProcess = processQueue()
.then(() => {
rebuildIndex();
_inProcess = null;
return objects;
});
}
return _inProcess;
};
//
// `locationSetID`
// Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world)
// (The locationset doesn't necessarily need to be resolved to compute its `id`)
//
// Arguments
// `locationSet`: A locationSet, e.g. `{ include: ['us'] }`
// Returns
// The locationSetID, e.g. `+[Q30]`
//
_this.locationSetID = (locationSet) => {
let locationSetID;
try {
locationSetID = _loco.validateLocationSet(locationSet).id;
} catch (err) {
locationSetID = '+[Q2]'; // the world
}
return locationSetID;
};
//
// `feature`
// Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world')
//
// Arguments
// `locationSetID`: id of the form like `+[Q30]` (United States)
// Returns
// A GeoJSON feature:
// {
// type: 'Feature',
// id: '+[Q30]',
// properties: { id: '+[Q30]', area: 21817019.17, … },
// geometry: { … }
// }
_this.feature = (locationSetID) => _resolvedFeatures[locationSetID] || _resolvedFeatures['+[Q2]'];
//
// `locationsAt`
// Find all the resolved locationSets valid at the given location.
// Results include the area (in km²) to facilitate sorting.
//
// Arguments
// `loc`: the [lon,lat] location to query, e.g. `[-74.4813, 40.7967]`
// Returns
// Object of locationSetIDs to areas (in km²)
// {
// "+[Q2]": 511207893.3958111,
// "+[Q30]": 21817019.17,
// "+[new_jersey.geojson]": 22390.77,
// …
// }
//
_this.locationsAt = (loc) => {
let result = {};
(_wp(loc, true) || []).forEach(prop => result[prop.id] = prop.area);
return result;
};
//
// `query`
// Execute a query directly against which-polygon
// https://github.com/mapbox/which-polygon
//
// Arguments
// `loc`: the [lon,lat] location to query,
// `multi`: `true` to return all results, `false` to return first result
// Returns
// Array of GeoJSON *properties* for the locationSet features that exist at `loc`
//
_this.query = (loc, multi) => _wp(loc, multi);
// Direct access to the location-conflation resolver
_this.loco = () => _loco;
// Direct access to the which-polygon index
_this.wp = () => _wp;
return _this;
}
+788 -510
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -113,10 +113,10 @@ export function modeMove(context, entityIDs, baseGraph) {
function cancel() {
if (baseGraph) {
while (context.graph() !== baseGraph) context.pop();
while (context.graph() !== baseGraph) context.pop(); // reset to baseGraph
context.enter(modeBrowse(context));
} else {
context.pop();
if (_prevGraph) context.pop(); // remove the move
context.enter(modeSelect(context, entityIDs));
}
stopNudge();
+2 -1
View File
@@ -119,7 +119,7 @@ export function modeRotate(context, entityIDs) {
function cancel() {
context.pop();
if (_prevGraph) context.pop(); // remove the rotate
context.enter(modeSelect(context, entityIDs));
}
@@ -130,6 +130,7 @@ export function modeRotate(context, entityIDs) {
mode.enter = function() {
_prevGraph = null;
context.features().forceVisible(entityIDs);
behaviors.forEach(context.install);
+95 -58
View File
@@ -23,14 +23,11 @@ import { osmNode, osmWay } from '../osm';
import * as Operations from '../operations/index';
import { uiCmd } from '../ui/cmd';
import {
utilArrayIntersection, utilDeepMemberSelector, utilEntityOrDeepMemberSelector,
utilArrayIntersection, utilArrayUnion, utilDeepMemberSelector, utilEntityOrDeepMemberSelector,
utilEntitySelector, utilKeybinding, utilTotalExtent, utilGetAllNodes
} from '../util';
var _relatedParent;
export function modeSelect(context, selectedIDs) {
var mode = {
id: 'select',
@@ -48,6 +45,11 @@ export function modeSelect(context, selectedIDs) {
var _newFeature = false;
var _follow = false;
// `_focusedParentWayId` is used when we visit a vertex with multiple
// parents, and we want to remember which parent line we started on.
var _focusedParentWayId;
var _focusedVertexIds;
function singular() {
if (selectedIDs && selectedIDs.length === 1) {
@@ -85,53 +87,50 @@ export function modeSelect(context, selectedIDs) {
}
// find the common parent ways for nextVertex, previousVertex
function commonParents() {
// find the parent ways for nextVertex, previousVertex, and selectParent
function parentWaysIdsOfSelection(onlyCommonParents) {
var graph = context.graph();
var commonParents = [];
var parents = [];
for (var i = 0; i < selectedIDs.length; i++) {
var entity = context.hasEntity(selectedIDs[i]);
if (!entity || entity.geometry(graph) !== 'vertex') {
return []; // selection includes some not vertices
return []; // selection includes some non-vertices
}
var currParents = graph.parentWays(entity).map(function(w) { return w.id; });
if (!commonParents.length) {
commonParents = currParents;
if (!parents.length) {
parents = currParents;
continue;
}
commonParents = utilArrayIntersection(commonParents, currParents);
if (!commonParents.length) {
parents = (onlyCommonParents ? utilArrayIntersection : utilArrayUnion)(parents, currParents);
if (!parents.length) {
return [];
}
}
return commonParents;
return parents;
}
function singularParent() {
var parents = commonParents();
if (!parents || parents.length === 0) {
_relatedParent = null;
return null;
function checkFocusedParent() {
if (_focusedParentWayId) {
var parents = parentWaysIdsOfSelection(true);
if (parents.indexOf(_focusedParentWayId) === -1) _focusedParentWayId = null;
}
}
function parentWayIdForVertexNavigation() {
var parentIds = parentWaysIdsOfSelection(true);
if (_focusedParentWayId && parentIds.indexOf(_focusedParentWayId) !== -1) {
// prefer the previously seen parent
return _focusedParentWayId;
}
// relatedParent is used when we visit a vertex with multiple
// parents, and we want to remember which parent line we started on.
if (parents.length === 1) {
_relatedParent = parents[0]; // remember this parent for later
return _relatedParent;
}
if (parents.indexOf(_relatedParent) !== -1) {
return _relatedParent; // prefer the previously seen parent
}
return parents[0];
return parentIds.length ? parentIds[0] : null;
}
@@ -244,7 +243,8 @@ export function modeSelect(context, selectedIDs) {
.on(utilKeybinding.plusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(Math.pow(1.05, 5)))
.on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧' + key)), scaleSelection(1/1.05))
.on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(1/Math.pow(1.05, 5)))
.on(['\\', 'pause'], nextParent)
.on(['\\', 'pause'], focusNextParent)
.on('|', selectParent)
.on('⎋', esc, true);
d3_select(document)
@@ -426,9 +426,10 @@ export function modeSelect(context, selectedIDs) {
surface.selectAll('.related')
.classed('related', false);
singularParent();
if (_relatedParent) {
surface.selectAll(utilEntitySelector([_relatedParent]))
// reload `_focusedParentWayId` based on the current selection
checkFocusedParent();
if (_focusedParentWayId) {
surface.selectAll(utilEntitySelector([_focusedParentWayId]))
.classed('related', true);
}
@@ -455,18 +456,20 @@ export function modeSelect(context, selectedIDs) {
function firstVertex(d3_event) {
d3_event.preventDefault();
var entity = singular();
var parent = singularParent();
var parentId = parentWayIdForVertexNavigation();
var way;
if (entity && entity.type === 'way') {
way = entity;
} else if (parent) {
way = context.entity(parent);
} else if (parentId) {
way = context.entity(parentId);
}
_focusedParentWayId = way && way.id;
if (way) {
context.enter(
modeSelect(context, [way.first()]).follow(true)
mode.selectedIDs([way.first()])
.follow(true)
);
}
}
@@ -475,18 +478,20 @@ export function modeSelect(context, selectedIDs) {
function lastVertex(d3_event) {
d3_event.preventDefault();
var entity = singular();
var parent = singularParent();
var parentId = parentWayIdForVertexNavigation();
var way;
if (entity && entity.type === 'way') {
way = entity;
} else if (parent) {
way = context.entity(parent);
} else if (parentId) {
way = context.entity(parentId);
}
_focusedParentWayId = way && way.id;
if (way) {
context.enter(
modeSelect(context, [way.last()]).follow(true)
mode.selectedIDs([way.last()])
.follow(true)
);
}
}
@@ -494,10 +499,11 @@ export function modeSelect(context, selectedIDs) {
function previousVertex(d3_event) {
d3_event.preventDefault();
var parent = singularParent();
if (!parent) return;
var parentId = parentWayIdForVertexNavigation();
_focusedParentWayId = parentId;
if (!parentId) return;
var way = context.entity(parent);
var way = context.entity(parentId);
var length = way.nodes.length;
var curr = way.nodes.indexOf(selectedIDs[0]);
var index = -1;
@@ -510,7 +516,8 @@ export function modeSelect(context, selectedIDs) {
if (index !== -1) {
context.enter(
modeSelect(context, [way.nodes[index]]).follow(true)
mode.selectedIDs([way.nodes[index]])
.follow(true)
);
}
}
@@ -518,10 +525,11 @@ export function modeSelect(context, selectedIDs) {
function nextVertex(d3_event) {
d3_event.preventDefault();
var parent = singularParent();
if (!parent) return;
var parentId = parentWayIdForVertexNavigation();
_focusedParentWayId = parentId;
if (!parentId) return;
var way = context.entity(parent);
var way = context.entity(parentId);
var length = way.nodes.length;
var curr = way.nodes.indexOf(selectedIDs[0]);
var index = -1;
@@ -534,40 +542,69 @@ export function modeSelect(context, selectedIDs) {
if (index !== -1) {
context.enter(
modeSelect(context, [way.nodes[index]]).follow(true)
mode.selectedIDs([way.nodes[index]])
.follow(true)
);
}
}
function nextParent(d3_event) {
function focusNextParent(d3_event) {
d3_event.preventDefault();
var parents = commonParents();
var parents = parentWaysIdsOfSelection(true);
if (!parents || parents.length < 2) return;
var index = parents.indexOf(_relatedParent);
var index = parents.indexOf(_focusedParentWayId);
if (index < 0 || index > parents.length - 2) {
_relatedParent = parents[0];
_focusedParentWayId = parents[0];
} else {
_relatedParent = parents[index + 1];
_focusedParentWayId = parents[index + 1];
}
var surface = context.surface();
surface.selectAll('.related')
.classed('related', false);
if (_relatedParent) {
surface.selectAll(utilEntitySelector([_relatedParent]))
if (_focusedParentWayId) {
surface.selectAll(utilEntitySelector([_focusedParentWayId]))
.classed('related', true);
}
}
function selectParent(d3_event) {
d3_event.preventDefault();
var currentSelectedIds = mode.selectedIDs();
var parentIds = _focusedParentWayId ? [_focusedParentWayId] : parentWaysIdsOfSelection(false);
if (!parentIds.length) {
var reselectIds = _focusedVertexIds && _focusedVertexIds.filter(id => context.hasEntity(id));
if (reselectIds && reselectIds.length) {
if (currentSelectedIds.length === 1) _focusedParentWayId = currentSelectedIds[0];
context.enter(
mode.selectedIDs(_focusedVertexIds)
);
}
} else {
context.enter(
mode.selectedIDs(parentIds)
);
// set this after re-entering the selection since we normally want it cleared on exit
_focusedVertexIds = currentSelectedIds;
}
}
};
mode.exit = function() {
// we could enter the mode multiple times but it's only new the first time
_newFeature = false;
_focusedVertexIds = null;
_operations.forEach(function(operation) {
if (operation.behavior) {
context.uninstall(operation.behavior);
+1 -1
View File
@@ -29,7 +29,7 @@ export function operationExtract(context, selectedIDs) {
_extent = _extent ? _extent.extend(entity.extent(graph)) : entity.extent(graph);
return actionExtract(entityID);
return actionExtract(entityID, context.projection);
}).filter(Boolean);
-4
View File
@@ -176,10 +176,6 @@ osmEntity.prototype = {
return Object.keys(this.tags).some(osmIsInterestingTag);
},
hasWikidata: function() {
return !!this.tags.wikidata || !!this.tags['brand:wikidata'];
},
isHighwayIntersection: function() {
return false;
},
+14 -6
View File
@@ -6,14 +6,15 @@ import { presetCollection } from './collection';
// `presetCategory` builds a `presetCollection` of member presets,
// decorated with some extra methods for searching and matching geometry
//
export function presetCategory(categoryID, category, all) {
export function presetCategory(categoryID, category, allPresets) {
let _this = Object.assign({}, category); // shallow copy
let _searchName; // cache
let _searchNameStripped; // cache
_this.id = categoryID;
_this.members = presetCollection(
category.members.map(presetID => all.item(presetID)).filter(Boolean)
(category.members || []).map(presetID => allPresets[presetID]).filter(Boolean)
);
_this.geometry = _this.members.collection
@@ -42,13 +43,20 @@ export function presetCategory(categoryID, category, all) {
_this.searchName = () => {
if (!_searchName) {
_searchName = (_this.suggestion ? _this.originalName : _this.name()).toLowerCase();
// split combined diacritical characters into their parts
if (_searchName.normalize) _searchName = _searchName.normalize('NFD');
// remove diacritics
_searchName = _searchName.replace(/[\u0300-\u036f]/g, '');
}
return _searchName;
};
_this.searchNameStripped = () => {
if (!_searchNameStripped) {
_searchNameStripped = _this.searchName();
// split combined diacritical characters into their parts
if (_searchNameStripped.normalize) _searchNameStripped = _searchNameStripped.normalize('NFD');
// remove diacritics
_searchNameStripped = _searchNameStripped.replace(/[\u0300-\u036f]/g, '');
}
return _searchNameStripped;
};
return _this;
}
+43 -32
View File
@@ -1,4 +1,6 @@
import { utilArrayUniq, utilEditDistance } from '../util';
import { locationManager } from '../core/locations';
import { utilArrayUniq } from '../util/array';
import { utilEditDistance } from '../util';
//
@@ -45,14 +47,11 @@ export function presetCollection(collection) {
return _this.item(id);
};
_this.search = (value, geometry, countryCode) => {
_this.search = (value, geometry, loc) => {
if (!value) return _this;
// don't remove diacritical characters since we're assuming the user is being intentional
value = value.toLowerCase().trim();
// split combined diacritical characters into their parts
if (value.normalize) value = value.normalize('NFD');
// remove diacritics
value = value.replace(/[\u0300-\u036f]/g, '');
// match at name beginning or just after a space (e.g. "office" -> match "Law Office")
function leading(a) {
@@ -66,53 +65,62 @@ export function presetCollection(collection) {
return index === 0;
}
function sortNames(a, b) {
let aCompare = a.searchName();
let bCompare = b.searchName();
function sortPresets(nameProp) {
return function sortNames(a, b) {
let aCompare = a[nameProp]();
let bCompare = b[nameProp]();
// priority if search string matches preset name exactly - #4325
if (value === aCompare) return -1;
if (value === bCompare) return 1;
// 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 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 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;
// priority for shorter preset names
return aCompare.length - bCompare.length;
};
}
let pool = _this.collection;
if (countryCode) {
pool = pool.filter(a => {
if (a.locationSet) {
if (a.locationSet.include && a.locationSet.include.indexOf(countryCode) === -1) return false;
if (a.locationSet.exclude && a.locationSet.exclude.indexOf(countryCode) !== -1) return false;
}
return true;
});
if (Array.isArray(loc)) {
const validLocations = locationManager.locationsAt(loc);
pool = pool.filter(a => !a.locationSetID || validLocations[a.locationSetID]);
}
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 leadingName = searchable
const leadingNames = searchable
.filter(a => leading(a.searchName()))
.sort(sortNames);
.sort(sortPresets('searchName'));
// matches value to preset suggestion name
const leadingSuggestions = suggestions
.filter(a => leadingStrict(a.searchName()))
.sort(sortNames);
.sort(sortPresets('searchName'));
const leadingNamesStripped = searchable
.filter(a => leading(a.searchNameStripped()))
.sort(sortPresets('searchNameStripped'));
const leadingSuggestionsStripped = suggestions
.filter(a => leadingStrict(a.searchNameStripped()))
.sort(sortPresets('searchNameStripped'));
// matches value to preset.terms values
const leadingTerms = searchable
.filter(a => (a.terms() || []).some(leading));
const leadingSuggestionTerms = suggestions
.filter(a => (a.terms() || []).some(leading));
// matches value to preset.tags values
const leadingTagValues = searchable
.filter(a => Object.values(a.tags || {}).filter(val => val !== '*').some(leading));
@@ -139,9 +147,12 @@ export function presetCollection(collection) {
});
});
let results = leadingName.concat(
let results = leadingNames.concat(
leadingSuggestions,
leadingNamesStripped,
leadingSuggestionsStripped,
leadingTerms,
leadingSuggestionTerms,
leadingTagValues,
similarName,
similarSuggestions,
+69 -17
View File
@@ -2,6 +2,8 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
import { prefs } from '../core/preferences';
import { fileFetcher } from '../core/file_fetcher';
import { locationManager } from '../core/locations';
import { osmNodeGeometriesForTags, osmSetAreaKeys, osmSetPointTags, osmSetVertexTags } from '../osm/tags';
import { presetCategory } from './category';
import { presetCollection } from './collection';
@@ -51,9 +53,9 @@ export function presetIndex() {
// Index of presets by (geometry, tag key).
let _geometryIndex = { point: {}, vertex: {}, line: {}, area: {}, relation: {} };
let _loadPromise;
_this.ensureLoaded = () => {
if (_loadPromise) return _loadPromise;
@@ -77,13 +79,27 @@ export function presetIndex() {
};
// `merge` accepts an object containing new preset data (all properties optional):
// {
// fields: {},
// presets: {},
// categories: {},
// defaults: {},
// featureCollection: {}
//}
_this.merge = (d) => {
let newLocationSets = [];
// Merge Fields
if (d.fields) {
Object.keys(d.fields).forEach(fieldID => {
const f = d.fields[fieldID];
let f = d.fields[fieldID];
if (f) { // add or replace
_fields[fieldID] = presetField(fieldID, f);
f = presetField(fieldID, f);
if (f.locationSet) newLocationSets.push(f);
_fields[fieldID] = f;
} else { // remove
delete _fields[fieldID];
}
@@ -93,10 +109,14 @@ export function presetIndex() {
// Merge Presets
if (d.presets) {
Object.keys(d.presets).forEach(presetID => {
const p = d.presets[presetID];
let p = d.presets[presetID];
if (p) { // add or replace
const isAddable = !_addablePresetIDs || _addablePresetIDs.has(presetID);
_presets[presetID] = presetPreset(presetID, p, isAddable, _fields, _presets);
p = presetPreset(presetID, p, isAddable, _fields, _presets);
if (p.locationSet) newLocationSets.push(p);
_presets[presetID] = p;
} else { // remove (but not if it's a fallback)
const existing = _presets[presetID];
if (existing && !existing.isFallback()) {
@@ -106,22 +126,23 @@ export function presetIndex() {
});
}
// Need to rebuild _this.collection before loading categories
_this.collection = Object.values(_presets).concat(Object.values(_categories));
// Merge Categories
if (d.categories) {
Object.keys(d.categories).forEach(categoryID => {
const c = d.categories[categoryID];
let c = d.categories[categoryID];
if (c) { // add or replace
_categories[categoryID] = presetCategory(categoryID, c, _this);
c = presetCategory(categoryID, c, _presets);
if (c.locationSet) newLocationSets.push(c);
_categories[categoryID] = c;
} else { // remove
delete _categories[categoryID];
}
});
}
// Rebuild _this.collection after loading categories
// Rebuild _this.collection after changing presets and categories
_this.collection = Object.values(_presets).concat(Object.values(_categories));
// Merge Defaults
@@ -155,6 +176,16 @@ export function presetIndex() {
});
});
// Merge Custom Features
if (d.featureCollection && Array.isArray(d.featureCollection.features)) {
locationManager.mergeCustomGeoJSON(d.featureCollection);
}
// Resolve all locationSet features.
if (newLocationSets.length) {
locationManager.mergeLocationSets(newLocationSets);
}
return _this;
};
@@ -166,17 +197,23 @@ export function presetIndex() {
if (geometry === 'vertex' && entity.isOnAddressLine(resolver)) {
geometry = 'point';
}
return _this.matchTags(entity.tags, geometry);
const entityExtent = entity.extent(resolver);
return _this.matchTags(entity.tags, geometry, entityExtent.center());
});
};
_this.matchTags = (tags, geometry) => {
_this.matchTags = (tags, geometry, loc) => {
const geometryMatches = _geometryIndex[geometry];
let address;
let best = -1;
let match;
let validLocations;
if (Array.isArray(loc)) {
validLocations = locationManager.locationsAt(loc);
}
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:*']) {
@@ -187,10 +224,17 @@ export function presetIndex() {
if (!keyMatches) continue;
for (let i = 0; i < keyMatches.length; i++) {
const score = keyMatches[i].matchScore(tags);
const candidate = keyMatches[i];
// discard candidate preset if location is not valid at `loc`
if (validLocations && candidate.locationSetID) {
if (!validLocations[candidate.locationSetID]) continue;
}
const score = candidate.matchScore(tags);
if (score > best) {
best = score;
match = keyMatches[i];
match = candidate;
}
}
}
@@ -313,11 +357,12 @@ export function presetIndex() {
_this.universal = () => _universal;
_this.defaults = (geometry, n, startWithRecents) => {
_this.defaults = (geometry, n, startWithRecents, loc) => {
let recents = [];
if (startWithRecents) {
recents = _this.recent().matchGeometry(geometry).collection.slice(0, 4);
}
let defaults;
if (_addablePresetIDs) {
defaults = Array.from(_addablePresetIDs).map(function(id) {
@@ -329,9 +374,16 @@ export function presetIndex() {
defaults = _defaults[geometry].collection.concat(_this.fallback(geometry));
}
return presetCollection(
let result = presetCollection(
utilArrayUniq(recents.concat(defaults)).slice(0, n - 1)
);
if (Array.isArray(loc)) {
const validLocations = locationManager.locationsAt(loc);
result.collection = result.collection.filter(a => !a.locationSetID || validLocations[a.locationSetID]);
}
return result;
};
// pass a Set of addable preset ids
+27 -8
View File
@@ -16,6 +16,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
let _resolvedFields; // cache
let _resolvedMoreFields; // cache
let _searchName; // cache
let _searchNameStripped; // cache
_this.id = presetID;
@@ -124,14 +125,21 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
_this.searchName = () => {
if (!_searchName) {
_searchName = (_this.suggestion ? _this.originalName : _this.name()).toLowerCase();
// split combined diacritical characters into their parts
if (_searchName.normalize) _searchName = _searchName.normalize('NFD');
// remove diacritics
_searchName = _searchName.replace(/[\u0300-\u036f]/g, '');
}
return _searchName;
};
_this.searchNameStripped = () => {
if (!_searchNameStripped) {
_searchNameStripped = _this.searchName();
// split combined diacritical characters into their parts
if (_searchNameStripped.normalize) _searchNameStripped = _searchNameStripped.normalize('NFD');
// remove diacritics
_searchNameStripped = _searchNameStripped.replace(/[\u0300-\u036f]/g, '');
}
return _searchNameStripped;
};
_this.isFallback = () => {
const tagCount = Object.keys(_this.tags).length;
return tagCount === 0 || (tagCount === 1 && _this.tags.hasOwnProperty('area'));
@@ -147,7 +155,13 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
_this.reference = () => {
// Lookup documentation on Wikidata...
const qid = _this.tags.wikidata || _this.tags['brand:wikidata'] || _this.tags['operator:wikidata'];
const qid = (
_this.tags.wikidata ||
_this.tags['flag:wikidata'] ||
_this.tags['brand:wikidata'] ||
_this.tags['network:wikidata'] ||
_this.tags['operator:wikidata']
);
if (qid) {
return { qid: qid };
}
@@ -164,8 +178,10 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
};
_this.unsetTags = (tags, geometry, skipFieldDefaults) => {
tags = utilObjectOmit(tags, Object.keys(_this.removeTags));
_this.unsetTags = (tags, geometry, ignoringKeys, skipFieldDefaults) => {
// allow manually keeping some tags
let removeTags = ignoringKeys ? utilObjectOmit(_this.removeTags, ignoringKeys) : _this.removeTags;
tags = utilObjectOmit(tags, Object.keys(removeTags));
if (geometry && !skipFieldDefaults) {
_this.fields().forEach(field => {
@@ -186,7 +202,10 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
for (let k in addTags) {
if (addTags[k] === '*') {
tags[k] = 'yes';
// if this tag is ancillary, don't override an existing value since any value is okay
if (_this.tags[k] || !tags[k] || tags[k] === 'no') {
tags[k] = 'yes';
}
} else {
tags[k] = addTags[k];
}
+1 -1
View File
@@ -174,7 +174,7 @@ export function rendererBackgroundSource(data) {
// WMS 1.3 flips x/y for some coordinate systems including EPSG:4326 - #7557
if (projection === 'EPSG:4326' &&
// The CRS parameter implies version 1.3 (prior versions use SRS)
/VERSION=1.3|CRS={proj}/.test(source.template())) {
/VERSION=1.3|CRS={proj}/.test(source.template().toUpperCase())) {
return maxXminY.y + ',' + minXmaxY.x + ',' + minXmaxY.y + ',' + maxXminY.x;
} else {
return minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y;
+9 -9
View File
@@ -589,15 +589,6 @@ export function rendererMap(context) {
return; // no change
}
var withinEditableZoom = map.withinEditableZoom();
if (_lastWithinEditableZoom !== withinEditableZoom) {
if (_lastWithinEditableZoom !== undefined) {
// notify that the map zoomed in or out over the editable zoom threshold
dispatch.call('crossEditableZoom', this, withinEditableZoom);
}
_lastWithinEditableZoom = withinEditableZoom;
}
if (geoScaleToZoom(k, TILESIZE) < _minzoom) {
surface.interrupt();
dispatch.call('hitMinZoom', this, map);
@@ -608,6 +599,15 @@ export function rendererMap(context) {
}
projection.transform(eventTransform);
var withinEditableZoom = map.withinEditableZoom();
if (_lastWithinEditableZoom !== withinEditableZoom) {
if (_lastWithinEditableZoom !== undefined) {
// notify that the map zoomed in or out over the editable zoom threshold
dispatch.call('crossEditableZoom', this, withinEditableZoom);
}
_lastWithinEditableZoom = withinEditableZoom;
}
var scale = k / _transformStart.k;
var tX = (x / scale - _transformStart.x) * scale;
+1 -1
View File
@@ -128,7 +128,7 @@ export function rendererPhotos(context) {
};
photos.shouldFilterByUsername = function() {
return showsLayer('mapillary') || showsLayer('openstreetcam') || showsLayer('streetside');
return !showsLayer('mapillary') && showsLayer('openstreetcam') && !showsLayer('streetside');
};
photos.showsPhotoType = function(val) {
+33 -30
View File
@@ -4,6 +4,7 @@ import serviceOsmose from './osmose';
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
import serviceNsi from './nsi';
import serviceOpenstreetcam from './openstreetcam';
import serviceOsm from './osm';
import serviceOsmWikibase from './osm_wikibase';
@@ -14,36 +15,38 @@ import serviceWikidata from './wikidata';
import serviceWikipedia from './wikipedia';
export var services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
improveOSM: serviceImproveOSM,
osmose: serviceOsmose,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
osmWikibase: serviceOsmWikibase,
maprules: serviceMapRules,
streetside: serviceStreetside,
taginfo: serviceTaginfo,
vectorTile: serviceVectorTile,
wikidata: serviceWikidata,
wikipedia: serviceWikipedia
export let services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
improveOSM: serviceImproveOSM,
osmose: serviceOsmose,
mapillary: serviceMapillary,
nsi: serviceNsi,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
osmWikibase: serviceOsmWikibase,
maprules: serviceMapRules,
streetside: serviceStreetside,
taginfo: serviceTaginfo,
vectorTile: serviceVectorTile,
wikidata: serviceWikidata,
wikipedia: serviceWikipedia
};
export {
serviceKeepRight,
serviceImproveOSM,
serviceOsmose,
serviceMapillary,
serviceMapRules,
serviceNominatim,
serviceOpenstreetcam,
serviceOsm,
serviceOsmWikibase,
serviceStreetside,
serviceTaginfo,
serviceVectorTile,
serviceWikidata,
serviceWikipedia
};
serviceKeepRight,
serviceImproveOSM,
serviceOsmose,
serviceMapillary,
serviceMapRules,
serviceNominatim,
serviceNsi,
serviceOpenstreetcam,
serviceOsm,
serviceOsmWikibase,
serviceStreetside,
serviceTaginfo,
serviceVectorTile,
serviceWikidata,
serviceWikipedia
};
File diff suppressed because it is too large Load Diff
+676
View File
@@ -0,0 +1,676 @@
import { Matcher } from 'name-suggestion-index';
import parseVersion from 'vparse';
import { fileFetcher, locationManager } from '../core';
import { presetManager } from '../presets';
// Make very sure this resolves to iD's `package.json`
// If you mess up the `../`s, the resolver may import another random package.json from somewhere else.
import packageJSON from '../../package.json';
// This service contains all the code related to the **name-suggestion-index** (aka NSI)
// NSI contains the most correct tagging for many commonly mapped features.
// See https://github.com/osmlab/name-suggestion-index and https://nsi.guide
// DATA
let _nsiStatus = 'loading'; // 'loading', 'ok', 'failed'
let _nsi = {};
// Sometimes we can upgrade a feature tagged like `building=yes` to a better tag.
const buildingPreset = {
'building/commercial': true,
'building/government': true,
'building/hotel': true,
'building/retail': true,
'building/office': true,
'building/supermarket': true,
'building/yes': true
};
// Exceptions to the namelike regexes.
// Usually a tag suffix contains a language code like `name:en`, `name:ru`
// but we want to exclude things like `operator:type`, `name:etymology`, etc..
const notNames = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|wikipedia)$/i;
// Exceptions to the branchlike regexes
const notBranches = /(coop|express|wireless|factory|outlet)/i;
// PRIVATE FUNCTIONS
// `setNsiSources()`
// Adds the sources to iD's filemap so we can start downloading data.
//
function setNsiSources() {
const nsiVersion = packageJSON.dependencies['name-suggestion-index'] || packageJSON.devDependencies['name-suggestion-index'];
const v = parseVersion(nsiVersion);
const vMinor = `${v.major}.${v.minor}`;
const sources = {
'nsi_data': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/nsi.min.json`,
'nsi_dissolved': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/dissolved.min.json`,
'nsi_features': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/featureCollection.min.json`,
'nsi_generics': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/genericWords.min.json`,
'nsi_presets': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/presets/nsi-id-presets.min.json`,
'nsi_replacements': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/replacements.min.json`,
'nsi_trees': `https://cdn.jsdelivr.net/npm/name-suggestion-index@${vMinor}/dist/trees.min.json`
};
let fileMap = fileFetcher.fileMap();
for (const k in sources) {
fileMap[k] = sources[k];
}
}
// `loadNsiPresets()`
// Returns a Promise fulfilled when the presets have been downloaded and merged into iD.
//
function loadNsiPresets() {
return (
Promise.all([
fileFetcher.get('nsi_presets'),
fileFetcher.get('nsi_features')
])
.then(vals => {
// Add `suggestion=true` to all the nsi presets
// The preset json schema doesn't include it, but the iD code still uses it
Object.values(vals[0].presets).forEach(preset => preset.suggestion = true);
presetManager.merge({
presets: vals[0].presets,
featureCollection: vals[1]
});
})
);
}
// `loadNsiData()`
// Returns a Promise fulfilled when the other data have been downloaded and processed
//
function loadNsiData() {
return (
Promise.all([
fileFetcher.get('nsi_data'),
fileFetcher.get('nsi_dissolved'),
fileFetcher.get('nsi_replacements'),
fileFetcher.get('nsi_trees')
])
.then(vals => {
_nsi = {
data: vals[0].nsi, // the raw name-suggestion-index data
dissolved: vals[1].dissolved, // list of dissolved items
replacements: vals[2].replacements, // trivial old->new qid replacements
trees: vals[3].trees, // metadata about trees, main tags
kvt: new Map(), // Map (k -> Map (v -> t) )
qids: new Map(), // Map (wd/wp tag values -> qids)
ids: new Map() // Map (id -> NSI item)
};
_nsi.matcher = new Matcher();
_nsi.matcher.buildMatchIndex(_nsi.data);
_nsi.matcher.buildLocationIndex(_nsi.data, locationManager.loco());
Object.keys(_nsi.data).forEach(tkv => {
const category = _nsi.data[tkv];
const parts = tkv.split('/', 3); // tkv = "tree/key/value"
const t = parts[0];
const k = parts[1];
const v = parts[2];
// Build a reverse index of keys -> values -> trees present in the name-suggestion-index
// Collect primary keys (e.g. "amenity", "craft", "shop", "man_made", "route", etc)
// "amenity": {
// "restaurant": "brands"
// }
let vmap = _nsi.kvt.get(k);
if (!vmap) {
vmap = new Map();
_nsi.kvt.set(k, vmap);
}
vmap.set(v, t);
const tree = _nsi.trees[t]; // e.g. "brands", "operators"
const mainTag = tree.mainTag; // e.g. "brand:wikidata", "operator:wikidata", etc
const items = category.items || [];
items.forEach(item => {
// Remember some useful things for later, cache NSI id -> item
item.tkv = tkv;
item.mainTag = mainTag;
_nsi.ids.set(item.id, item);
// Cache Wikidata/Wikipedia values -> qid, for #6416
const wd = item.tags[mainTag];
const wp = item.tags[mainTag.replace('wikidata', 'wikipedia')];
if (wd) _nsi.qids.set(wd, wd);
if (wp && wd) _nsi.qids.set(wp, wd);
});
});
})
);
}
// `gatherKVs()`
// Gather all the k/v pairs that we will run through the NSI matcher.
// An OSM tags object can contain anything, but only a few tags will be interesting to NSI.
//
// This function will return the interesting tag pairs like:
// "amenity/restaurant", "man_made/flagpole"
// and fallbacks like
// "amenity/yes"
// excluding things like
// "highway", "surface", "ref", etc.
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// Returns
// `Object` containing kv pairs to test:
// {
// 'primary': Set(),
// 'alternate': Set()
// }
//
function gatherKVs(tags) {
let primary = new Set();
let alternate = new Set();
Object.keys(tags).forEach(osmkey => {
const osmvalue = tags[osmkey];
if (!osmvalue) return;
const vmap = _nsi.kvt.get(osmkey);
if (!vmap) return;
if (osmvalue !== 'yes') {
primary.add(`${osmkey}/${osmvalue}`);
} else {
alternate.add(`${osmkey}/${osmvalue}`);
}
});
// Can we try a generic building fallback match? - See #6122, #7197
// Only try this if we do a preset match and find nothing else remarkable about that building.
// For example, a way with `building=yes` + `name=Westfield` may be a Westfield department store.
// But a way with `building=yes` + `name=Westfield` + `public_transport=station` is a train station for a town named "Westfield"
const preset = presetManager.matchTags(tags, 'area');
if (buildingPreset[preset.id]) {
alternate.add('building/yes');
}
return { primary: primary, alternate: alternate };
}
// `identifyTree()`
// NSI has a concept of trees: "brands", "operators", "flags", "transit".
// The tree determines things like which tags are namelike, and which tags hold important wikidata.
// This takes an Object of tags and tries to identify what tree to use.
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// Returns
// `string` the name of the tree if known
// or 'unknown' if it could match several trees (e.g. amenity/yes)
// or null if no match
//
function identifyTree(tags) {
let unknown;
let t;
// Check all tags
Object.keys(tags).forEach(osmkey => {
if (t) return; // found already
const osmvalue = tags[osmkey];
if (!osmvalue) return;
const vmap = _nsi.kvt.get(osmkey);
if (!vmap) return; // this key is not in nsi
if (osmvalue === 'yes') {
unknown = 'unknown';
} else {
t = vmap.get(osmvalue);
}
});
return t || unknown || null;
}
// `gatherNames()`
// Gather all the namelike values that we will run through the NSI matcher.
// It will gather values primarily from tags `name`, `name:ru`, `flag:name`
// and fallback to alternate tags like `brand`, `brand:ru`, `alt_name`
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// Returns
// `Object` containing namelike values to test:
// {
// 'primary': Set(),
// 'fallbacks': Set()
// }
//
function gatherNames(tags) {
const empty = { primary: new Set(), alternate: new Set() };
let primary = new Set();
let alternate = new Set();
let foundSemi = false;
let testNameFragments = false;
let patterns;
// Patterns for matching OSM keys that might contain namelike values.
// These roughly correspond to the "trees" concept in name-suggestion-index,
let t = identifyTree(tags);
if (!t) return empty;
if (t === 'transit') {
patterns = {
primary: /^network$/i,
alternate: /^(operator|operator:\w+|network:\w+|\w+_name|\w+_name:\w+)$/i
};
} else if (t === 'flags') {
patterns = {
primary: /^(flag:name|flag:name:\w+)$/i,
alternate: /^(flag|flag:\w+|subject|subject:\w+)$/i // note: no `country`, we special-case it below
};
} else if (t === 'brands') {
testNameFragments = true;
patterns = {
primary: /^(name|name:\w+)$/i,
alternate: /^(brand|brand:\w+|operator|operator:\w+|\w+_name|\w+_name:\w+)/i,
};
} else if (t === 'operators') {
testNameFragments = true;
patterns = {
primary: /^(name|name:\w+|operator|operator:\w+)$/i,
alternate: /^(brand|brand:\w+|\w+_name|\w+_name:\w+)/i,
};
} else { // unknown/multiple
testNameFragments = true;
patterns = {
primary: /^(name|name:\w+)$/i,
alternate: /^(brand|brand:\w+|network|network:\w+|operator|operator:\w+|\w+_name|\w+_name:\w+)/i,
};
}
// Test `name` fragments, longest to shortest, to fit them into a "Name Branch" pattern.
// e.g. "TUI ReiseCenter - Neuss Innenstadt" -> ["TUI", "ReiseCenter", "Neuss", "Innenstadt"]
if (tags.name && testNameFragments) {
const nameParts = tags.name.split(/[\s\-\/,.]/);
for (let split = nameParts.length; split > 0; split--) {
const name = nameParts.slice(0, split).join(' '); // e.g. "TUI ReiseCenter"
primary.add(name);
}
}
// Check all tags
Object.keys(tags).forEach(osmkey => {
const osmvalue = tags[osmkey];
if (!osmvalue) return;
if (isNamelike(osmkey, 'primary')) {
if (/;/.test(osmvalue)) {
foundSemi = true;
} else {
primary.add(osmvalue);
alternate.delete(osmvalue);
}
} else if (!primary.has(osmvalue) && isNamelike(osmkey, 'alternate')) {
if (/;/.test(osmvalue)) {
foundSemi = true;
} else {
alternate.add(osmvalue);
}
}
});
// For flags only, fallback to `country` tag only if no other namelike values were found.
// See https://github.com/openstreetmap/iD/pull/8305#issuecomment-769174070
if (tags.man_made === 'flagpole' && !primary.size && !alternate.size && !!tags.country) {
const osmvalue = tags.country;
if (/;/.test(osmvalue)) {
foundSemi = true;
} else {
alternate.add(osmvalue);
}
}
// If any namelike value contained a semicolon, return empty set and don't try matching anything.
if (foundSemi) {
return empty;
} else {
return { primary: primary, alternate: alternate };
}
function isNamelike(osmkey, which) {
return patterns[which].test(osmkey) && !notNames.test(osmkey);
}
}
// `gatherTuples()`
// Generate all combinations of [key,value,name] that we want to test.
// This prioritizes them so that the primary name and k/v pairs go first
//
// Arguments
// `tryKVs`: `Object` containing primary and alternate k/v pairs to test
// `tryNames`: `Object` containing primary and alternate names to test
// Returns
// `Array`: tuple objects ordered by priority
//
function gatherTuples(tryKVs, tryNames) {
let tuples = [];
['primary', 'alternate'].forEach(whichName => {
// test names longest to shortest
const arr = Array.from(tryNames[whichName]).sort((a, b) => b.length - a.length);
arr.forEach(n => {
['primary', 'alternate'].forEach(whichKV => {
tryKVs[whichKV].forEach(kv => {
const parts = kv.split('/', 2);
const k = parts[0];
const v = parts[1];
tuples.push({ k: k, v: v, n: n });
});
});
});
});
return tuples;
}
// `_upgradeTags()`
// Try to match a feature to a canonical record in name-suggestion-index
// and upgrade the tags to match.
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// `loc`: Location where this feature exists, as a [lon, lat]
// Returns
// `Object`: The tags the the feature should have, or `null` if no changes needed
//
function _upgradeTags(tags, loc) {
let newTags = Object.assign({}, tags); // shallow copy
let changed = false;
// Before anything, perform trivial Wikipedia/Wikidata replacements
Object.keys(newTags).forEach(osmkey => {
const matchTag = osmkey.match(/^(\w+:)?wikidata$/);
if (matchTag) { // Look at '*:wikidata' tags
const prefix = (matchTag[1] || '');
const wd = newTags[osmkey];
const replace = _nsi.replacements[wd]; // If it matches a QID in the replacement list...
if (replace && replace.wikidata !== undefined) { // replace or delete `*:wikidata` tag
changed = true;
if (replace.wikidata) {
newTags[osmkey] = replace.wikidata;
} else {
delete newTags[osmkey];
}
}
if (replace && replace.wikipedia !== undefined) { // replace or delete `*:wikipedia` tag
changed = true;
const wpkey = `${prefix}wikipedia`;
if (replace.wikipedia) {
newTags[wpkey] = replace.wikipedia;
} else {
delete newTags[wpkey];
}
}
}
});
// Gather key/value tag pairs to try to match
const tryKVs = gatherKVs(tags);
if (!tryKVs.primary.size && !tryKVs.alternate.size) return changed ? newTags : null;
// Gather namelike tag values to try to match
const tryNames = gatherNames(tags);
// Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416
// If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.
const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);
if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too
if (!tryNames.primary.size && !tryNames.alternate.size) return changed ? newTags : null;
// Order the [key,value,name] tuples - test primary before alternate
const tuples = gatherTuples(tryKVs, tryNames);
for (let i = 0; i < tuples.length; i++) {
const tuple = tuples[i];
const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc); // Attempt to match an item in NSI
if (!hits || !hits.length) continue; // no match, try next tuple
if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break; // a generic match, stop looking
// A match may contain multiple results, the first one is likely the best one for this location
// e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`
let itemID, item;
for (let j = 0; j < hits.length; j++) {
const hit = hits[j];
itemID = hit.itemID;
if (_nsi.dissolved[itemID]) continue; // Don't upgrade to a dissolved item
item = _nsi.ids.get(itemID);
if (!item) continue;
const mainTag = item.mainTag; // e.g. `brand:wikidata`
const itemQID = item.tags[mainTag]; // e.g. `brand:wikidata` qid
const notQID = newTags[`not:${mainTag}`]; // e.g. `not:brand:wikidata` qid
if ( // Exceptions, skip this hit
(!itemQID || itemQID === notQID) || // No `*:wikidata` or matched a `not:*:wikidata`
(newTags.office && !item.tags.office) // feature may be a corporate office for a brand? - #6416
) {
item = null;
continue; // continue looking
} else {
break; // use `item`
}
}
// Can't use any of these hits, try next tuple..
if (!item) continue;
// At this point we have matched a canonical item and can suggest tag upgrades..
const tkv = item.tkv;
const parts = tkv.split('/', 3); // tkv = "tree/key/value"
const k = parts[1];
const v = parts[2];
const category = _nsi.data[tkv];
const properties = category.properties || {};
// Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)
const preserveTags = item.preserveTags || properties.preserveTags || [];
let regexes = preserveTags.map(s => new RegExp(s, 'i'));
regexes.push(/^building$/i, /^takeaway$/i);
let keepTags = {};
Object.keys(newTags).forEach(osmkey => {
if (regexes.some(regex => regex.test(osmkey))) {
keepTags[osmkey] = newTags[osmkey];
}
});
// Remove any primary tags ("amenity", "craft", "shop", "man_made", "route", etc)
// with a value like `amenity=yes` or `shop=yes`
_nsi.kvt.forEach((vmap, k) => {
if (newTags[k] === 'yes') delete newTags[k];
});
// Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`
if (foundQID) {
delete newTags.wikipedia;
delete newTags.wikidata;
}
// Do the tag upgrade
Object.assign(newTags, item.tags, keepTags);
// Special `branch` splitting rules - IF..
// - NSI is suggesting to replace `name`, AND
// - `branch` doesn't already contain something, AND
// - original name has not moved to an alternate name (e.g. "Dunkin' Donuts" -> "Dunkin'"), AND
// - original name is "some name" + "some stuff", THEN
// consider splitting `name` into `name`/`branch`..
const origName = tags.name;
const newName = newTags.name;
if (newName && origName && newName !== origName && !newTags.branch) {
const newNames = gatherNames(newTags);
const newSet = new Set([...newNames.primary, ...newNames.alternate]);
const isMoved = newSet.has(origName); // another tag holds the original name now
if (!isMoved) {
// Test name fragments, longest to shortest, to fit them into a "Name Branch" pattern.
// e.g. "TUI ReiseCenter - Neuss Innenstadt" -> ["TUI", "ReiseCenter", "Neuss", "Innenstadt"]
const nameParts = origName.split(/[\s\-\/,.]/);
for (let split = nameParts.length; split > 0; split--) {
const name = nameParts.slice(0, split).join(' '); // e.g. "TUI ReiseCenter"
const branch = nameParts.slice(split).join(' '); // e.g. "Neuss Innenstadt"
const nameHits = _nsi.matcher.match(k, v, name, loc);
if (!nameHits || !nameHits.length) continue; // no match, try next name fragment
if (nameHits.some(hit => hit.itemID === itemID)) { // matched the name fragment to the same itemID above
if (branch) {
if (notBranches.test(branch)) { // "branch" was detected but is noise ("factory outlet", etc)
newTags.name = origName; // Leave `name` alone, this part of the name may be significant..
} else {
const branchHits = _nsi.matcher.match(k, v, branch, loc);
if (branchHits && branchHits.length) { // if "branch" matched something else in NSI..
if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') { // if another brand! (e.g. "KFC - Taco Bell"?)
return null; // bail out - can't suggest tags in this case
} // else a generic (e.g. "gas", "cafe") - ignore
} else { // "branch" is not noise and not something in NSI
newTags.branch = branch; // Stick it in the `branch` tag..
}
}
}
break;
}
}
}
}
return newTags;
}
return changed ? newTags : null;
}
// `_isGenericName()`
// Is the `name` tag generic?
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// Returns
// `true` if it is generic, `false` if not
//
function _isGenericName(tags) {
const n = tags.name;
if (!n) return false;
// tryNames just contains the `name` tag value and nothing else
const tryNames = { primary: new Set([n]), alternate: new Set() };
// Gather key/value tag pairs to try to match
const tryKVs = gatherKVs(tags);
if (!tryKVs.primary.size && !tryKVs.alternate.size) return false;
// Order the [key,value,name] tuples - test primary before alternate
const tuples = gatherTuples(tryKVs, tryNames);
for (let i = 0; i < tuples.length; i++) {
const tuple = tuples[i];
const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n); // Attempt to match an item in NSI
// If we get a `excludeGeneric` hit, this is a generic name.
if (hits && hits.length && hits[0].match === 'excludeGeneric') return true;
}
return false;
}
// PUBLIC INTERFACE
export default {
// `init()`
// On init, start preparing the name-suggestion-index
//
init: () => {
// Note: service.init is called immediately after the presetManager has started loading its data.
// We expect to chain onto an unfulfilled promise here.
setNsiSources();
presetManager.ensureLoaded()
.then(() => loadNsiPresets())
.then(() => delay(100)) // wait briefly for locationSets to enter the locationManager queue
.then(() => locationManager.mergeLocationSets([])) // wait for locationSets to resolve
.then(() => loadNsiData())
.then(() => _nsiStatus = 'ok')
.catch(() => _nsiStatus = 'failed');
function delay(msec) {
return new Promise(resolve => {
window.setTimeout(resolve, msec);
});
}
},
// `reset()`
// Reset is called when user saves data to OSM (does nothing here)
//
reset: () => {},
// `status()`
// To let other code know how it's going...
//
// Returns
// `String`: 'loading', 'ok', 'failed'
//
status: () => _nsiStatus,
// `isGenericName()`
// Is the `name` tag generic?
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// Returns
// `true` if it is generic, `false` if not
//
isGenericName: (tags) => _isGenericName(tags),
// `upgradeTags()`
// Suggest tag upgrades.
// This function will not modify the input tags, it makes a copy.
//
// Arguments
// `tags`: `Object` containing the feature's OSM tags
// `loc`: Location where this feature exists, as a [lon, lat]
// Returns
// `Object`: The tags the the feature should have, or `null` if no change
//
upgradeTags: (tags, loc) => _upgradeTags(tags, loc),
// `cache()`
// Direct access to the NSI cache, useful for testing or breaking things
//
// Returns
// `Object`: the internal NSI cache
//
cache: () => _nsi
};
+1 -1
View File
@@ -544,7 +544,7 @@ export default {
.classed('currentView', function(d) { return d.properties.key === selectedSequenceKey; });
// update viewfields if needed
context.container().selectAll('.viewfield-group .viewfield')
context.container().selectAll('.layer-openstreetcam .viewfield-group .viewfield')
.attr('d', viewfieldPath);
function viewfieldPath() {
+11 -6
View File
@@ -260,6 +260,7 @@ function parseJSON(payload, callback, options) {
var children = json.elements;
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
var results = [];
var result;
for (var i = 0; i < children.length; i++) {
@@ -268,7 +269,6 @@ function parseJSON(payload, callback, options) {
}
callback(null, results);
});
_deferred.add(handle);
function parseChild(child) {
@@ -296,13 +296,12 @@ function parseUserJSON(payload, callback, options) {
var json = payload;
if (typeof json !== 'object') json = JSON.parse(payload);
if (!json.elements) return callback({ message: 'No JSON', status: -1 });
if (!json.users && !json.user) return callback({ message: 'No JSON', status: -1 });
var objs = json.users || [json];
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
var results = [];
var result;
for (var i = 0; i < objs.length; i++) {
@@ -311,7 +310,6 @@ function parseUserJSON(payload, callback, options) {
}
callback(null, results);
});
_deferred.add(handle);
function parseObj(obj) {
@@ -459,6 +457,7 @@ function parseXML(xml, callback, options) {
var children = root.childNodes;
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
var results = [];
var result;
for (var i = 0; i < children.length; i++) {
@@ -467,7 +466,6 @@ function parseXML(xml, callback, options) {
}
callback(null, results);
});
_deferred.add(handle);
@@ -663,7 +661,14 @@ export default {
} else {
var url = urlroot + path;
var controller = new AbortController();
d3_json(url, { signal: controller.signal })
var fn;
if (path.indexOf('.json') !== -1) {
fn = d3_json;
} else {
fn = d3_xml;
}
fn(url, { signal: controller.signal })
.then(function(data) {
done(null, data);
})
+1 -1
View File
@@ -942,7 +942,7 @@ export default {
.classed('currentView', d => d.properties.key === selectedSequenceKey);
// update viewfields if needed
context.container().selectAll('.viewfield-group .viewfield')
context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')
.attr('d', viewfieldPath);
function viewfieldPath() {
+3 -3
View File
@@ -3,7 +3,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
import deepEqual from 'fast-deep-equal';
import turf_bboxClip from '@turf/bbox-clip';
import stringify from 'fast-json-stable-stringify';
import * as martinez from 'martinez-polygon-clipping';
import polygonClipping from 'polygon-clipping';
import Protobuf from 'pbf';
import vt from '@mapbox/vector-tile';
@@ -66,13 +66,13 @@ function vtToGeoJSON(data, tile, mergeCache) {
var merged = mergeCache[propertyhash];
if (merged && merged.length) {
var other = merged[0];
var coords = martinez.union(
var coords = polygonClipping.union(
feature.geometry.coordinates,
other.geometry.coordinates
);
if (!coords || !coords.length) {
continue; // something failed in martinez union
continue; // something failed in polygon union
}
merged.push(feature);
+1 -1
View File
@@ -550,7 +550,7 @@ export function svgLabels(projection, context) {
function getAreaLabel(entity, width, height) {
var centroid = path.centroid(entity.asGeoJSON(graph, true));
var centroid = path.centroid(entity.asGeoJSON(graph));
var extent = entity.extent(graph);
var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];
+54 -84
View File
@@ -6,13 +6,12 @@ import { services } from '../services';
export function svgMapillaryImages(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var minMarkerZoom = 16;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
var viewerCompassAngle;
const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
const minZoom = 12;
const minMarkerZoom = 16;
const minViewfieldZoom = 18;
let layer = d3_select(null);
let _mapillary;
function init() {
@@ -35,7 +34,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
function showLayer() {
var service = getService();
const service = getService();
if (!service) return;
editOn();
@@ -71,39 +70,38 @@ export function svgMapillaryImages(projection, context, dispatch) {
}
function click(d3_event, d) {
var service = getService();
function click(d3_event, image) {
const service = getService();
if (!service) return;
service
.ensureViewerLoaded(context)
.then(function() {
service
.selectImage(context, d.key)
.selectImage(context, image.id)
.showViewer(context);
});
context.map().centerEase(d.loc);
context.map().centerEase(image.loc);
}
function mouseover(d) {
var service = getService();
if (service) service.setStyles(context, d);
function mouseover(d3_event, image) {
const service = getService();
if (service) service.setStyles(context, image);
}
function mouseout() {
var service = getService();
const service = getService();
if (service) service.setStyles(context, null);
}
function transform(d) {
var t = svgPointTransform(projection)(d);
if (d.pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
} else if (d.ca) {
let t = svgPointTransform(projection)(d);
if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
}
return t;
@@ -111,82 +109,54 @@ export function svgMapillaryImages(projection, context, dispatch) {
function filterImages(images) {
var showsPano = context.photos().showsPanoramic();
var showsFlat = context.photos().showsFlat();
var fromDate = context.photos().fromDate();
var toDate = context.photos().toDate();
var usernames = context.photos().usernames();
const showsPano = context.photos().showsPanoramic();
const showsFlat = context.photos().showsFlat();
const fromDate = context.photos().fromDate();
const toDate = context.photos().toDate();
if (!showsPano || !showsFlat) {
images = images.filter(function(image) {
if (image.pano) return showsPano;
if (image.is_pano) return showsPano;
return showsFlat;
});
}
if (fromDate) {
var fromTimestamp = new Date(fromDate).getTime();
images = images.filter(function(image) {
return new Date(image.captured_at).getTime() >= fromTimestamp;
return new Date(image.captured_at).getTime() >= new Date(fromDate).getTime();
});
}
if (toDate) {
var toTimestamp = new Date(toDate).getTime();
images = images.filter(function(image) {
return new Date(image.captured_at).getTime() <= toTimestamp;
});
}
if (usernames) {
images = images.filter(function(image) {
return usernames.indexOf(image.captured_by) !== -1;
return new Date(image.captured_at).getTime() <= new Date(toDate).getTime();
});
}
return images;
}
function filterSequences(sequences, service) {
var showsPano = context.photos().showsPanoramic();
var showsFlat = context.photos().showsFlat();
var fromDate = context.photos().fromDate();
var toDate = context.photos().toDate();
var usernames = context.photos().usernames();
function filterSequences(sequences) {
const showsPano = context.photos().showsPanoramic();
const showsFlat = context.photos().showsFlat();
const fromDate = context.photos().fromDate();
const toDate = context.photos().toDate();
if (!showsPano || !showsFlat) {
sequences = sequences.filter(function(sequence) {
if (sequence.properties.hasOwnProperty('pano')) {
if (sequence.properties.pano) return showsPano;
if (sequence.properties.hasOwnProperty('is_pano')) {
if (sequence.properties.is_pano) return showsPano;
return showsFlat;
} else {
// if the sequence doesn't specify pano or not, search its images
var cProps = sequence.properties.coordinateProperties;
if (cProps && cProps.image_keys && cProps.image_keys.length > 0) {
for (var index in cProps.image_keys) {
var imageKey = cProps.image_keys[index];
var image = service.cachedImage(imageKey);
if (image && image.hasOwnProperty('pano')) {
if (image.pano) return showsPano;
return showsFlat;
}
}
}
}
return false;
});
}
if (fromDate) {
var fromTimestamp = new Date(fromDate).getTime();
sequences = sequences.filter(function(sequence) {
return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp;
return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();
});
}
if (toDate) {
var toTimestamp = new Date(toDate).getTime();
sequences = sequences.filter(function(sequence) {
return new Date(sequence.properties.captured_at).getTime() <= toTimestamp;
});
}
if (usernames) {
sequences = sequences.filter(function(sequence) {
return usernames.indexOf(sequence.properties.username) !== -1;
return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();
});
}
@@ -195,20 +165,21 @@ export function svgMapillaryImages(projection, context, dispatch) {
function update() {
var z = ~~context.map().zoom();
var showMarkers = (z >= minMarkerZoom);
var showViewfields = (z >= minViewfieldZoom);
const z = ~~context.map().zoom();
const showMarkers = (z >= minMarkerZoom);
const showViewfields = (z >= minViewfieldZoom);
var service = getService();
var sequences = (service ? service.sequences(projection) : []);
var images = (service && showMarkers ? service.images(projection) : []);
const service = getService();
let sequences = (service ? service.sequences(projection) : []);
let images = (service && showMarkers ? service.images(projection) : []);
images = filterImages(images);
sequences = filterSequences(sequences, service);
service.filterViewer(context);
var traces = layer.selectAll('.sequences').selectAll('.sequence')
.data(sequences, function(d) { return d.properties.key; });
let traces = layer.selectAll('.sequences').selectAll('.sequence')
.data(sequences, function(d) { return d.properties.id; });
// exit
traces.exit()
@@ -222,15 +193,15 @@ export function svgMapillaryImages(projection, context, dispatch) {
.attr('d', svgPath(projection).geojson);
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(images, function(d) { return d.key; });
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(images, function(d) { return d.id; });
// exit
groups.exit()
.remove();
// enter
var groupsEnter = groups.enter()
const groupsEnter = groups.enter()
.append('g')
.attr('class', 'viewfield-group')
.on('mouseenter', mouseover)
@@ -242,7 +213,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
.attr('class', 'viewfield-scale');
// update
var markers = groups
const markers = groups
.merge(groupsEnter)
.sort(function(a, b) {
return b.loc[1] - a.loc[1]; // sort Y
@@ -259,7 +230,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
.attr('dy', '0')
.attr('r', '6');
var viewfields = markers.selectAll('.viewfield')
const viewfields = markers.selectAll('.viewfield')
.data(showViewfields ? [0] : []);
viewfields.exit()
@@ -268,13 +239,12 @@ export function svgMapillaryImages(projection, context, dispatch) {
viewfields.enter() // viewfields may or may not be drawn...
.insert('path', 'circle') // but if they are, draw below the circles
.attr('class', 'viewfield')
.classed('pano', function() { return this.parentNode.__data__.pano; })
.classed('pano', function() { return this.parentNode.__data__.is_pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
function viewfieldPath() {
var d = this.parentNode.__data__;
if (d.pano) {
if (this.parentNode.__data__.is_pano) {
return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';
} else {
return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';
@@ -284,8 +254,8 @@ export function svgMapillaryImages(projection, context, dispatch) {
function drawImages(selection) {
var enabled = svgMapillaryImages.enabled;
var service = getService();
const enabled = svgMapillaryImages.enabled;
const service = getService();
layer = selection.selectAll('.layer-mapillary')
.data(service ? [0] : []);
@@ -293,7 +263,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
layer.exit()
.remove();
var layerEnter = layer.enter()
const layerEnter = layer.enter()
.append('g')
.attr('class', 'layer-mapillary')
.style('display', enabled ? 'block' : 'none');
+38 -76
View File
@@ -5,10 +5,10 @@ import { services } from '../services';
import { t } from '../core/localizer';
export function svgMapillaryMapFeatures(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _mapillary;
const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
const minZoom = 12;
let layer = d3_select(null);
let _mapillary;
function init() {
@@ -30,7 +30,7 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
function showLayer() {
var service = getService();
const service = getService();
if (!service) return;
service.loadObjectResources(context);
@@ -56,88 +56,69 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
function click(d3_event, d) {
var service = getService();
const service = getService();
if (!service) return;
context.map().centerEase(d.loc);
var selectedImageKey = service.getSelectedImageKey();
var imageKey;
var highlightedDetection;
// Pick one of the images the map feature was detected in,
// preference given to an image already selected.
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
const selectedImageId = service.getActiveImage() && service.getActiveImage().id;
service.getDetections(d.id).then(detections => {
if (detections.length) {
const imageId = detections[0].image.id;
if (imageId === selectedImageId) {
service
.highlightDetection(detections[0])
.selectImage(context, imageId);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(detections[0])
.selectImage(context, imageId)
.showViewer(context);
});
}
}
});
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
function filterData(detectedFeatures) {
var service = getService();
var fromDate = context.photos().fromDate();
var toDate = context.photos().toDate();
var usernames = context.photos().usernames();
const fromDate = context.photos().fromDate();
const toDate = context.photos().toDate();
if (fromDate) {
var fromTimestamp = new Date(fromDate).getTime();
detectedFeatures = detectedFeatures.filter(function(feature) {
return new Date(feature.last_seen_at).getTime() >= fromTimestamp;
return new Date(feature.last_seen_at).getTime() >= new Date(fromDate).getTime();
});
}
if (toDate) {
var toTimestamp = new Date(toDate).getTime();
detectedFeatures = detectedFeatures.filter(function(feature) {
return new Date(feature.first_seen_at).getTime() <= toTimestamp;
});
}
if (usernames && service) {
detectedFeatures = detectedFeatures.filter(function(feature) {
return feature.detections.some(function(detection) {
var imageKey = detection.image_key;
var image = service.cachedImage(imageKey);
return image && usernames.indexOf(image.captured_by) !== -1;
});
return new Date(feature.first_seen_at).getTime() <= new Date(toDate).getTime();
});
}
return detectedFeatures;
}
function update() {
var service = getService();
var data = (service ? service.mapFeatures(projection) : []);
const service = getService();
let data = (service ? service.mapFeatures(projection) : []);
data = filterData(data);
var selectedImageKey = service && service.getSelectedImageKey();
var transform = svgPointTransform(projection);
const transform = svgPointTransform(projection);
var mapFeatures = layer.selectAll('.icon-map-feature')
.data(data, function(d) { return d.key; });
const mapFeatures = layer.selectAll('.icon-map-feature')
.data(data, function(d) { return d.id; });
// exit
mapFeatures.exit()
.remove();
// enter
var enter = mapFeatures.enter()
const enter = mapFeatures.enter()
.append('g')
.attr('class', 'icon-map-feature icon-detected')
.on('click', click);
@@ -173,32 +154,13 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
// update
mapFeatures
.merge(enter)
.attr('transform', transform)
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
})
.sort(function(a, b) {
var aSelected = a.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
var bSelected = b.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
if (aSelected === bSelected) {
return b.loc[1] - a.loc[1]; // sort Y
} else if (aSelected) {
return 1;
}
return -1;
});
.attr('transform', transform);
}
function drawMapFeatures(selection) {
var enabled = svgMapillaryMapFeatures.enabled;
var service = getService();
const enabled = svgMapillaryMapFeatures.enabled;
const service = getService();
layer = selection.selectAll('.layer-mapillary-map-features')
.data(service ? [0] : []);
+23 -33
View File
@@ -6,12 +6,12 @@ import { services } from '../services';
export function svgMapillaryPosition(projection, context) {
var throttledRedraw = _throttle(function () { update(); }, 1000);
var minZoom = 12;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
var viewerCompassAngle;
const throttledRedraw = _throttle(function () { update(); }, 1000);
const minZoom = 12;
const minViewfieldZoom = 18;
let layer = d3_select(null);
let _mapillary;
let viewerCompassAngle;
function init() {
@@ -23,15 +23,15 @@ export function svgMapillaryPosition(projection, context) {
function getService() {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('nodeChanged', throttledRedraw);
_mapillary.event.on('imageChanged', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
viewerCompassAngle = e.bearing;
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
return d.is_pano;
})
.attr('transform', transform);
});
@@ -54,8 +54,8 @@ export function svgMapillaryPosition(projection, context) {
function transform(d) {
var t = svgPointTransform(projection)(d);
if (d.pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
let t = svgPointTransform(projection)(d);
if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
} else if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
@@ -65,21 +65,21 @@ export function svgMapillaryPosition(projection, context) {
function update() {
var z = ~~context.map().zoom();
var showViewfields = (z >= minViewfieldZoom);
const z = ~~context.map().zoom();
const showViewfields = (z >= minViewfieldZoom);
var service = getService();
var node = service && service.getActiveImage();
const service = getService();
const image = service && service.getActiveImage();
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(node ? [node] : [], function(d) { return d.key; });
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(image ? [image] : [], function(d) { return d.id; });
// exit
groups.exit()
.remove();
// enter
var groupsEnter = groups.enter()
const groupsEnter = groups.enter()
.append('g')
.attr('class', 'viewfield-group currentView highlighted');
@@ -89,7 +89,7 @@ export function svgMapillaryPosition(projection, context) {
.attr('class', 'viewfield-scale');
// update
var markers = groups
const markers = groups
.merge(groupsEnter)
.attr('transform', transform)
.select('.viewfield-scale');
@@ -103,7 +103,7 @@ export function svgMapillaryPosition(projection, context) {
.attr('dy', '0')
.attr('r', '6');
var viewfields = markers.selectAll('.viewfield')
const viewfields = markers.selectAll('.viewfield')
.data(showViewfields ? [0] : []);
viewfields.exit()
@@ -112,23 +112,13 @@ export function svgMapillaryPosition(projection, context) {
viewfields.enter()
.insert('path', 'circle')
.attr('class', 'viewfield')
.classed('pano', function() { return this.parentNode.__data__.pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
function viewfieldPath() {
var d = this.parentNode.__data__;
if (d.pano) {
return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';
} else {
return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';
}
}
.attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');
}
function drawImages(selection) {
var service = getService();
const service = getService();
layer = selection.selectAll('.layer-mapillary-position')
.data(service ? [0] : []);
@@ -136,7 +126,7 @@ export function svgMapillaryPosition(projection, context) {
layer.exit()
.remove();
var layerEnter = layer.enter()
const layerEnter = layer.enter()
.append('g')
.attr('class', 'layer-mapillary-position');
+34 -71
View File
@@ -5,10 +5,10 @@ import { services } from '../services';
export function svgMapillarySigns(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _mapillary;
const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
const minZoom = 12;
let layer = d3_select(null);
let _mapillary;
function init() {
@@ -30,7 +30,7 @@ export function svgMapillarySigns(projection, context, dispatch) {
function showLayer() {
var service = getService();
const service = getService();
if (!service) return;
service.loadSignResources(context);
@@ -56,46 +56,37 @@ export function svgMapillarySigns(projection, context, dispatch) {
function click(d3_event, d) {
var service = getService();
const service = getService();
if (!service) return;
context.map().centerEase(d.loc);
var selectedImageKey = service.getSelectedImageKey();
var imageKey;
var highlightedDetection;
// Pick one of the images the sign was detected in,
// preference given to an image already selected.
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
const selectedImageId = service.getActiveImage() && service.getActiveImage().id;
service.getDetections(d.id).then(detections => {
if (detections.length) {
const imageId = detections[0].image.id;
if (imageId === selectedImageId) {
service
.highlightDetection(detections[0])
.selectImage(context, imageId);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(detections[0])
.selectImage(context, imageId)
.showViewer(context);
});
}
}
});
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
function filterData(detectedFeatures) {
var service = getService();
var fromDate = context.photos().fromDate();
var toDate = context.photos().toDate();
var usernames = context.photos().usernames();
if (fromDate) {
var fromTimestamp = new Date(fromDate).getTime();
@@ -109,36 +100,27 @@ export function svgMapillarySigns(projection, context, dispatch) {
return new Date(feature.first_seen_at).getTime() <= toTimestamp;
});
}
if (usernames && service) {
detectedFeatures = detectedFeatures.filter(function(feature) {
return feature.detections.some(function(detection) {
var imageKey = detection.image_key;
var image = service.cachedImage(imageKey);
return image && usernames.indexOf(image.captured_by) !== -1;
});
});
}
return detectedFeatures;
}
function update() {
var service = getService();
var data = (service ? service.signs(projection) : []);
const service = getService();
let data = (service ? service.signs(projection) : []);
data = filterData(data);
var selectedImageKey = service.getSelectedImageKey();
var transform = svgPointTransform(projection);
const transform = svgPointTransform(projection);
var signs = layer.selectAll('.icon-sign')
.data(data, function(d) { return d.key; });
const signs = layer.selectAll('.icon-sign')
.data(data, function(d) { return d.id; });
// exit
signs.exit()
.remove();
// enter
var enter = signs.enter()
const enter = signs.enter()
.append('g')
.attr('class', 'icon-sign icon-detected')
.on('click', click);
@@ -161,32 +143,13 @@ export function svgMapillarySigns(projection, context, dispatch) {
// update
signs
.merge(enter)
.attr('transform', transform)
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
})
.sort(function(a, b) {
var aSelected = a.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
var bSelected = b.detections.some(function(detection) {
return detection.image_key === selectedImageKey;
});
if (aSelected === bSelected) {
return b.loc[1] - a.loc[1]; // sort Y
} else if (aSelected) {
return 1;
}
return -1;
});
.attr('transform', transform);
}
function drawSigns(selection) {
var enabled = svgMapillarySigns.enabled;
var service = getService();
const enabled = svgMapillarySigns.enabled;
const service = getService();
layer = selection.selectAll('.layer-mapillary-signs')
.data(service ? [0] : []);
+9 -1
View File
@@ -160,7 +160,15 @@ export function svgTagClasses() {
}
// If this is a wikidata-tagged item, add a class for that..
if (t.wikidata || t['brand:wikidata']) {
var qid = (
t.wikidata ||
t['flag:wikidata'] ||
t['brand:wikidata'] ||
t['network:wikidata'] ||
t['operator:wikidata']
);
if (qid) {
classes.push('tag-wikidata');
}
+1 -1
View File
@@ -91,7 +91,7 @@ export function uiFeatureList(context) {
if (d3_event.keyCode === 13 && // ↩ Return
q.length &&
items.size()) {
click(items.datum());
click(d3_event, items.datum());
}
}
+12 -26
View File
@@ -1,8 +1,8 @@
import * as countryCoder from '@ideditor/country-coder';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { t, localizer } from '../core/localizer';
import { locationManager } from '../core/locations';
import { svgIcon } from '../svg/icon';
import { uiTooltip } from './tooltip';
import { geoExtent } from '../geo/extent';
@@ -28,6 +28,14 @@ export function uiField(context, presetField, entityIDs, options) {
var _state = '';
var _tags = {};
var _entityExtent;
if (entityIDs && entityIDs.length) {
_entityExtent = entityIDs.reduce(function(extent, entityID) {
var entity = context.graph().entity(entityID);
return extent.extend(entity.extent(context.graph()));
}, geoExtent());
}
var _locked = false;
var _lockedTip = uiTooltip()
.title(t.html('inspector.lock.suggestion', { label: field.label }))
@@ -301,23 +309,9 @@ export function uiField(context, presetField, entityIDs, options) {
return field.matchGeometry(context.graph().geometry(entityID));
})) return false;
if (field.locationSet) {
var extent = combinedEntityExtent();
if (!extent) return true;
var center = extent.center();
var countryCode = countryCoder.iso1A2Code(center);
if (!countryCode) return false;
countryCode = countryCode.toLowerCase();
if (field.locationSet.include && field.locationSet.include.indexOf(countryCode) === -1) {
return false;
}
if (field.locationSet.exclude && field.locationSet.exclude.indexOf(countryCode) !== -1) {
return false;
}
if (entityIDs && _entityExtent && field.locationSetID) { // is field allowed in this location?
var validLocations = locationManager.locationsAt(_entityExtent.center());
if (!validLocations[field.locationSetID]) return false;
}
var prerequisiteTag = field.prerequisiteTag;
@@ -356,13 +350,5 @@ export function uiField(context, presetField, entityIDs, options) {
};
function combinedEntityExtent() {
return entityIDs && entityIDs.length && entityIDs.reduce(function(extent, entityID) {
var entity = context.graph().entity(entityID);
return extent.extend(entity.extent(context.graph()));
}, geoExtent());
}
return utilRebind(field, dispatch, 'on');
}
+3 -3
View File
@@ -329,9 +329,9 @@ export function uiFieldCombo(field, context) {
var listClass = 'chiplist';
// Use a separate line for each value in the Destinations field
// Use a separate line for each value in the Destinations and Via fields
// to mimic highway exit signs
if (field.key === 'destination') {
if (field.key === 'destination' || field.key === 'via') {
listClass += ' full-line-chips';
}
@@ -601,7 +601,7 @@ export function uiFieldCombo(field, context) {
var targetIndexOffsetTop = null;
var draggedTagWidth = d3_select(this).node().offsetWidth;
if (field.key === 'destination') { // meaning tags are full width
if (field.key === 'destination' || field.key === 'via') { // meaning tags are full width
_container.selectAll('.chip')
.style('transform', function(d2, index2) {
var node = d3_select(this).node();
+26 -4
View File
@@ -34,11 +34,33 @@ export function uiFieldText(field, context) {
.catch(function() { /* ignore */ });
}
function i(selection) {
var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]);
var preset = entity && presetManager.match(entity, context.graph());
var isLocked = preset && preset.suggestion && field.id === 'brand';
function calcLocked() {
// Protect certain fields that have a companion `*:wikidata` value
var isLocked = (field.id === 'brand' || field.id === 'network' || field.id === 'operator' || field.id === 'flag') &&
_entityIDs.length &&
_entityIDs.some(function(entityID) {
var entity = context.graph().hasEntity(entityID);
if (!entity) return false;
// Features linked to Wikidata are likely important and should be protected
if (entity.tags.wikidata) return true;
var preset = presetManager.match(entity, context.graph());
var isSuggestion = preset && preset.suggestion;
// Lock the field if there is a value and a companion `*:wikidata` value
var which = field.id; // 'brand', 'network', 'operator', 'flag'
return isSuggestion && !!entity.tags[which] && !!entity.tags[which + ':wikidata'];
});
field.locked(isLocked);
}
function i(selection) {
calcLocked();
var isLocked = field.locked();
var wrap = selection.selectAll('.form-field-input-wrap')
.data([0]);
+25 -155
View File
@@ -9,7 +9,7 @@ import { services } from '../../services';
import { svgIcon } from '../../svg';
import { uiTooltip } from '../tooltip';
import { uiCombobox } from '../combobox';
import { utilArrayUniq, utilEditDistance, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUniqueDomId } from '../../util';
import { utilArrayUniq, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUniqueDomId } from '../../util';
var _languagesArray = [];
@@ -35,20 +35,11 @@ export function uiFieldLocalized(field, context) {
.then(function(d) { _territoryLanguages = d; })
.catch(function() { /* ignore */ });
var allSuggestions = presetManager.collection.filter(function(p) {
return p.suggestion === true;
});
// reuse these combos
var langCombo = uiCombobox(context, 'localized-lang')
.fetcher(fetchLanguages)
.minItems(0);
var brandCombo = uiCombobox(context, 'localized-brand')
.canAutocomplete(false)
.minItems(1);
var _selection = d3_select(null);
var _multilingual = [];
var _buttonTip = uiTooltip()
@@ -83,34 +74,40 @@ export function uiFieldLocalized(field, context) {
function calcLocked() {
// only lock the Name field
var isLocked = field.id === 'name' &&
// Protect name field for suggestion presets that don't display a brand/operator field
var isLocked = (field.id === 'name') &&
_entityIDs.length &&
// lock the field if any feature needs it
_entityIDs.some(function(entityID) {
var entity = context.graph().hasEntity(entityID);
if (!entity) return false;
var original = context.graph().base().entities[_entityIDs[0]];
var hasOriginalName = original && entity.tags.name && entity.tags.name === original.tags.name;
// if the name was already edited manually then allow further editing
if (!hasOriginalName) return false;
// features linked to Wikidata are likely important and should be protected
// Features linked to Wikidata are likely important and should be protected
if (entity.tags.wikidata) return true;
// assume the name has already been confirmed if its source has been researched
// Assume the name has already been confirmed if its source has been researched
if (entity.tags['name:etymology:wikidata']) return true;
// Lock the `name` if this is a suggestion preset that assigns the name,
// and the preset does not display a `brand` or `operator` field.
// (For presets like hotels, car dealerships, post offices, the `name` should remain editable)
// see also similar logic in `outdated_tags.js`
var preset = presetManager.match(entity, context.graph());
var isSuggestion = preset && preset.suggestion;
var showsBrand = preset && preset.originalFields.filter(function(d) {
return d.id === 'brand';
}).length;
// protect standardized brand names
return isSuggestion && !showsBrand;
if (preset) {
var isSuggestion = preset.suggestion;
var fields = preset.fields();
var showsBrandField = fields.some(function(d) { return d.id === 'brand'; });
var showsOperatorField = fields.some(function(d) { return d.id === 'operator'; });
var setsName = preset.addTags.name;
var setsBrandWikidata = preset.addTags['brand:wikidata'];
var setsOperatorWikidata = preset.addTags['operator:wikidata'];
return (isSuggestion && setsName && (
(setsBrandWikidata && !showsBrandField) ||
(setsOperatorWikidata && !showsOperatorField)
));
}
return false;
});
field.locked(isLocked);
@@ -152,8 +149,6 @@ export function uiFieldLocalized(field, context) {
_selection = selection;
calcLocked();
var isLocked = field.locked();
var singularEntity = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]);
var preset = singularEntity && presetManager.match(singularEntity, context.graph());
var wrap = selection.selectAll('.form-field-input-wrap')
.data([0]);
@@ -176,39 +171,6 @@ export function uiFieldLocalized(field, context) {
.call(utilNoAuto)
.merge(input);
if (preset && field.id === 'name') {
var pTag = preset.id.split('/', 2);
var pKey = pTag[0];
var pValue = pTag[1];
if (!preset.suggestion) {
// Not a suggestion preset - Add a suggestions dropdown if it makes sense to.
// This code attempts to determine if the matched preset is the
// kind of preset that even can benefit from name suggestions..
// - true = shops, cafes, hotels, etc. (also generic and fallback presets)
// - false = churches, parks, hospitals, etc. (things not in the index)
var isFallback = preset.isFallback();
var goodSuggestions = allSuggestions.filter(function(s) {
if (isFallback) return true;
var sTag = s.id.split('/', 2);
var sKey = sTag[0];
var sValue = sTag[1];
return pKey === sKey && (!pValue || pValue === sValue);
});
// Show the suggestions.. If the user picks one, change the tags..
if (allSuggestions.length && goodSuggestions.length) {
input
.on('blur.localized', checkBrandOnBlur)
.call(brandCombo
.fetcher(fetchBrandNames(preset, allSuggestions))
.on('accept', acceptBrand)
.on('cancel', cancelBrand)
);
}
}
}
input
.classed('disabled', !!isLocked)
.attr('readonly', isLocked || null)
@@ -253,98 +215,6 @@ export function uiFieldLocalized(field, context) {
// We are not guaranteed to get an `accept` or `cancel` when blurring the field.
// (This can happen if the user actives the combo, arrows down, and then clicks off to blur)
// So compare the current field value against the suggestions one last time.
function checkBrandOnBlur() {
var latest = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]);
if (!latest) return; // deleting the entity blurred the field?
var preset = presetManager.match(latest, context.graph());
if (preset && preset.suggestion) return; // already accepted
var name = utilGetSetValue(input).trim();
var matched = allSuggestions.filter(function(s) { return name === s.name(); });
if (matched.length === 1) {
acceptBrand({ suggestion: matched[0] });
} else {
cancelBrand();
}
}
function acceptBrand(d) {
var entity = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]);
if (!d || !entity) {
cancelBrand();
return;
}
var tags = entity.tags;
var geometry = entity.geometry(context.graph());
var removed = preset.unsetTags(tags, geometry);
for (var k in tags) {
tags[k] = removed[k]; // set removed tags to `undefined`
}
tags = d.suggestion.setTags(tags, geometry);
utilGetSetValue(input, tags.name);
dispatch.call('change', this, tags);
}
// user hit escape
function cancelBrand() {
var name = utilGetSetValue(input);
dispatch.call('change', this, { name: name });
}
function fetchBrandNames(preset, suggestions) {
var pTag = preset.id.split('/', 2);
var pKey = pTag[0];
var pValue = pTag[1];
return function(value, callback) {
var results = [];
if (value && value.length > 2) {
for (var i = 0; i < suggestions.length; i++) {
var s = suggestions[i];
// don't suggest brands from incompatible countries
if (_countryCode && s.countryCodes &&
s.countryCodes.indexOf(_countryCode) === -1) continue;
var sTag = s.id.split('/', 2);
var sKey = sTag[0];
var sValue = sTag[1];
var subtitle = s.subtitle();
var name = s.name();
if (subtitle) name += ' ' + subtitle;
var dist = utilEditDistance(value, name.substring(0, value.length));
var matchesPreset = (pKey === sKey && (!pValue || pValue === sValue));
if (dist < 1 || (matchesPreset && dist < 3)) {
var obj = {
value: s.name(),
title: name,
display: s.nameLabel() + (subtitle ? ' ' + s.subtitleLabel() : ''),
suggestion: s,
dist: dist + (matchesPreset ? 0 : 1) // penalize if not matched preset
};
results.push(obj);
}
}
results.sort(function(a, b) { return a.dist - b.dist; });
}
results = results.slice(0, 10);
callback(results);
};
}
function addNew(d3_event) {
d3_event.preventDefault();
if (field.locked()) return;
+6 -4
View File
@@ -1,8 +1,7 @@
import {
geoLength as d3_geoLength,
geoCentroid as d3_geoCentroid
geoPath as d3_geoPath
} from 'd3-geo';
import geojsonRewind from '@mapbox/geojson-rewind';
import { t, localizer } from '../../core/localizer';
import { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';
@@ -79,8 +78,11 @@ export function uiPanelMeasurement(context) {
closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());
var feature = entity.asGeoJSON(graph);
length += radiansToMeters(d3_geoLength(toLineString(feature)));
// d3_geoCentroid is wrong for counterclockwise-wound polygons, so wind them clockwise
centroid = d3_geoCentroid(geojsonRewind(Object.assign({}, feature), true));
centroid = d3_geoPath(context.projection).centroid(entity.asGeoJSON(graph));
centroid = centroid && context.projection.invert(centroid);
if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {
centroid = entity.extent(graph).center();
}
if (closed) {
area += steradiansToSqmeters(entity.area(graph));
}
+1
View File
@@ -379,6 +379,7 @@ export function uiPresetIcon() {
horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],
light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],
monorail: ['railway/monorail', 'railway/monorail', 'railway/monorail'],
mtb: ['highway/path', 'highway/track', 'highway/bridleway'],
pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],
piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],
power: ['power/line', 'power/line', 'power/line'],
+19 -20
View File
@@ -1,9 +1,5 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import * as countryCoder from '@ideditor/country-coder';
import {
select as d3_select
} from 'd3-selection';
import { select as d3_select } from 'd3-selection';
import { presetManager } from '../presets';
import { t, localizer } from '../core/localizer';
@@ -20,6 +16,7 @@ import { utilKeybinding, utilNoAuto, utilRebind } from '../util';
export function uiPresetList(context) {
var dispatch = d3_dispatch('cancel', 'choose');
var _entityIDs;
var _currLoc;
var _currentPresets;
var _autofocus = false;
@@ -94,19 +91,16 @@ export function uiPresetList(context) {
function inputevent() {
var value = search.property('value');
list.classed('filtered', value.length);
var extent = combinedEntityExtent();
var results, messageText;
if (value.length && extent) {
var center = extent.center();
var countryCode = countryCoder.iso1A2Code(center);
results = presets.search(value, entityGeometries()[0], countryCode);
var results, messageText;
if (value.length) {
results = presets.search(value, entityGeometries()[0], _currLoc);
messageText = t('inspector.results', {
n: results.collection.length,
search: value
});
} else {
results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro());
results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc);
messageText = t('inspector.choose');
}
list.call(drawList, results);
@@ -147,7 +141,7 @@ export function uiPresetList(context) {
var list = listWrap
.append('div')
.attr('class', 'preset-list')
.call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro()));
.call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc));
context.features().on('change.preset-list', updateForFeatureHiddenState);
}
@@ -483,13 +477,25 @@ export function uiPresetList(context) {
presetList.entityIDs = function(val) {
if (!arguments.length) return _entityIDs;
_entityIDs = val;
_currLoc = null;
if (_entityIDs && _entityIDs.length) {
// calculate current location
const extent = _entityIDs.reduce(function(extent, entityID) {
var entity = context.graph().entity(entityID);
return extent.extend(entity.extent(context.graph()));
}, geoExtent());
_currLoc = extent.center();
// match presets
var presets = _entityIDs.map(function(entityID) {
return presetManager.match(context.entity(entityID), context.graph());
});
presetList.presets(presets);
}
return presetList;
};
@@ -522,12 +528,5 @@ export function uiPresetList(context) {
});
}
function combinedEntityExtent() {
return _entityIDs.reduce(function(extent, entityID) {
var entity = context.graph().entity(entityID);
return extent.extend(entity.extent(context.graph()));
}, geoExtent());
}
return utilRebind(presetList, dispatch, 'on');
}
+12 -3
View File
@@ -1,17 +1,24 @@
import { select as d3_select } from 'd3-selection';
import { prefs } from '../../core/preferences';
import { svgIcon } from '../../svg/icon';
import { utilArrayIdentical } from '../../util/array';
import { t } from '../../core/localizer';
import { utilHighlightEntities } from '../../util';
import { uiSection } from '../section';
export function uiSectionEntityIssues(context) {
// Does the user prefer to expand the active issue? Useful for viewing tag diff.
// Expand by default so first timers see it - #6408, #8143
var preference = prefs('entity-issues.reference.expanded');
var _expanded = preference === null ? true : (preference === 'true');
var _entityIDs = [];
var _issues = [];
var _activeIssueID;
var section = uiSection('entity-issues', context)
.shouldDisplay(function() {
return _issues.length > 0;
@@ -122,6 +129,8 @@ export function uiSectionEntityIssues(context) {
var container = d3_select(this.parentNode.parentNode.parentNode);
var info = container.selectAll('.issue-info');
var isExpanded = info.classed('expanded');
_expanded = !isExpanded;
prefs('entity-issues.reference.expanded', _expanded); // update preference
if (isExpanded) {
info
@@ -151,9 +160,9 @@ export function uiSectionEntityIssues(context) {
containersEnter
.append('div')
.attr('class', 'issue-info')
.style('max-height', '0')
.style('opacity', '0')
.attr('class', 'issue-info' + (_expanded ? ' expanded' : ''))
.style('max-height', (_expanded ? null : '0'))
.style('opacity', (_expanded ? '1' : '0'))
.each(function(d) {
if (typeof d.reference === 'function') {
d3_select(this)
+1 -1
View File
@@ -175,7 +175,7 @@ export function uiSectionValidationRules(context) {
.property('value', degStr);
prefs('validate-square-degrees', degStr);
context.validator().reloadUnsquareIssues();
context.validator().revalidateUnsquare();
}
function isRuleEnabled(d) {
+53 -60
View File
@@ -1,11 +1,12 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import LocationConflation from '@ideditor/location-conflation';
import whichPolygon from 'which-polygon';
import { resolveStrings } from 'osm-community-index';
import { fileFetcher } from '../core/file_fetcher';
import { locationManager } from '../core/locations';
import { t, localizer } from '../core/localizer';
import { svgIcon } from '../svg/icon';
import { uiDisclosure } from '../ui/disclosure';
import { utilRebind } from '../util/rebind';
@@ -23,30 +24,37 @@ export function uiSuccess(context) {
function ensureOSMCommunityIndex() {
const data = fileFetcher;
return Promise.all([ data.get('oci_resources'), data.get('oci_features') ])
return Promise.all([
data.get('oci_features'),
data.get('oci_resources'),
data.get('oci_defaults')
])
.then(vals => {
if (_oci) return _oci;
const ociResources = vals[0].resources;
const loco = new LocationConflation(vals[1]);
let ociFeatures = {};
// Merge Custom Features
if (vals[0] && Array.isArray(vals[0].features)) {
locationManager.mergeCustomGeoJSON(vals[0]);
}
Object.values(ociResources).forEach(resource => {
const feature = loco.resolveLocationSet(resource.locationSet).feature;
let ociFeature = ociFeatures[feature.id];
if (!ociFeature) {
ociFeature = JSON.parse(JSON.stringify(feature)); // deep clone
ociFeature.properties.resourceIDs = new Set();
ociFeatures[feature.id] = ociFeature;
}
ociFeature.properties.resourceIDs.add(resource.id);
});
return _oci = {
features: ociFeatures,
resources: ociResources,
query: whichPolygon({ type: 'FeatureCollection', features: Object.values(ociFeatures) })
};
let ociResources = Object.values(vals[1].resources);
if (ociResources.length) {
// Resolve all locationSet features.
return locationManager.mergeLocationSets(ociResources)
.then(() => {
_oci = {
resources: ociResources,
defaults: vals[2].defaults
};
return _oci;
});
} else {
_oci = {
resources: [], // no resources?
defaults: vals[2].defaults
};
return _oci;
}
});
}
@@ -150,19 +158,23 @@ export function uiSuccess(context) {
// Get OSM community index features intersecting the map..
ensureOSMCommunityIndex()
.then(oci => {
let communities = [];
const properties = oci.query(context.map().center(), true) || [];
const loc = context.map().center();
const validLocations = locationManager.locationsAt(loc);
// Gather the communities from the result
properties.forEach(props => {
const resourceIDs = Array.from(props.resourceIDs);
resourceIDs.forEach(resourceID => {
const resource = oci.resources[resourceID];
communities.push({
area: props.area || Infinity,
order: resource.order || 0,
resource: resource
});
// Gather the communities
let communities = [];
oci.resources.forEach(resource => {
let area = validLocations[resource.locationSetID];
if (!area) return;
// Resolve strings
const localizer = (stringID) => t.html(`community.${stringID}`);
resource.resolved = resolveStrings(resource, oci.defaults, localizer);
communities.push({
area: area,
order: resource.order || 0,
resource: resource
});
});
@@ -200,7 +212,7 @@ export function uiSuccess(context) {
.attr('class', 'cell-icon community-icon')
.append('a')
.attr('target', '_blank')
.attr('href', d => d.url)
.attr('href', d => d.resolved.url)
.append('svg')
.attr('class', 'logo-small')
.append('use')
@@ -230,32 +242,19 @@ export function uiSuccess(context) {
function showCommunityDetails(d) {
let selection = d3_select(this);
let communityID = d.id;
let replacements = {
url: linkify(d.url),
signupUrl: linkify(d.signupUrl || d.url)
};
selection
.append('div')
.attr('class', 'community-name')
.append('a')
.attr('target', '_blank')
.attr('href', d.url)
.html(t.html(`community.${d.id}.name`));
let descriptionHTML = t.html(`community.${d.id}.description`, replacements);
if (d.type === 'reddit') { // linkify subreddits #4997
descriptionHTML = descriptionHTML
.replace(/(\/r\/\w*\/*)/i, match => linkify(d.url, match));
}
.html(d.resolved.nameHTML);
selection
.append('div')
.attr('class', 'community-description')
.html(descriptionHTML);
.html(d.resolved.descriptionHTML);
if (d.extendedDescription || (d.languageCodes && d.languageCodes.length)) {
// Create an expanding section if any of these are present..
if (d.resolved.extendedDescriptionHTML || (d.languageCodes && d.languageCodes.length)) {
selection
.append('div')
.call(uiDisclosure(context, `community-more-${d.id}`, false)
@@ -305,11 +304,11 @@ export function uiSuccess(context) {
.append('div')
.attr('class', 'community-more');
if (d.extendedDescription) {
if (d.resolved.extendedDescriptionHTML) {
moreEnter
.append('div')
.attr('class', 'community-extended-description')
.html(t.html(`community.${d.id}.extendedDescription`, replacements));
.html(d.resolved.extendedDescriptionHTML);
}
if (d.languageCodes && d.languageCodes.length) {
@@ -385,12 +384,6 @@ export function uiSuccess(context) {
return description;
});
}
function linkify(url, text) {
text = text || url;
return `<a target="_blank" href="${url}">${text}</a>`;
}
}
+1
View File
@@ -26,6 +26,7 @@ export { utilEntitySelector } from './util';
export { utilEntityOrMemberSelector } from './util';
export { utilEntityOrDeepMemberSelector } from './util';
export { utilFastMouse } from './util';
export { utilFetchJson } from './util';
export { utilFunctor } from './util';
export { utilGetAllNodes } from './util';
export { utilGetSetValue } from './get_set_value';
+2
View File
@@ -370,6 +370,8 @@ utilKeybinding.keyCodes = {
'+': 107, 'plus': 107,
// Num-Subtract, or -
'-': 109, subtract: 109,
// Vertical Bar / Pipe
'|': 124,
// Firefox Plus
'ffplus': 171,
// Firefox Minus
+61 -15
View File
@@ -182,15 +182,42 @@ export function utilGetAllNodes(ids, graph) {
export function utilDisplayName(entity) {
var localizedNameKey = 'name:' + localizer.languageCode().toLowerCase();
var name = entity.tags[localizedNameKey] || entity.tags.name || '';
var network = entity.tags.cycle_network || entity.tags.network;
if (name) return name;
if (!name && entity.tags.ref) {
name = entity.tags.ref;
if (network) {
name = network + ' ' + name;
var tags = {
direction: entity.tags.direction,
from: entity.tags.from,
network: entity.tags.cycle_network || entity.tags.network,
ref: entity.tags.ref,
to: entity.tags.to,
via: entity.tags.via
};
var keyComponents = [];
if (tags.network) {
keyComponents.push('network');
}
if (tags.ref) {
keyComponents.push('ref');
}
// Routes may need more disambiguation based on direction or destination
if (entity.tags.route) {
if (tags.direction) {
keyComponents.push('direction');
} else if (tags.from && tags.to) {
keyComponents.push('from');
keyComponents.push('to');
if (tags.via) {
keyComponents.push('via');
}
}
}
if (keyComponents.length) {
name = t('inspector.display_name.' + keyComponents.join('_'), tags);
}
return name;
}
@@ -216,21 +243,29 @@ export function utilDisplayType(id) {
}
export function utilDisplayLabel(entity, graphOrGeometry) {
// `utilDisplayLabel`
// Returns a string suitable for display
// By default returns something like name/ref, fallback to preset type, fallback to OSM type
// "Main Street" or "Tertiary Road"
// If `verbose=true`, include both preset name and feature name.
// "Tertiary Road Main Street"
//
export function utilDisplayLabel(entity, graphOrGeometry, verbose) {
var result;
var displayName = utilDisplayName(entity);
if (displayName) {
// use the display name if there is one
return displayName;
}
var preset = typeof graphOrGeometry === 'string' ?
presetManager.matchTags(entity.tags, graphOrGeometry) :
presetManager.match(entity, graphOrGeometry);
if (preset && preset.name()) {
// use the preset name if there is a match
return preset.name();
var presetName = preset && (preset.suggestion ? preset.subtitle() : preset.name());
if (verbose) {
result = [presetName, displayName].filter(Boolean).join(' ');
} else {
result = displayName || presetName;
}
// fallback to the display type (node/way/relation)
return utilDisplayType(entity.id);
// Fallback to the OSM type (node/way/relation)
return result || utilDisplayType(entity.id);
}
@@ -545,3 +580,14 @@ export function utilUnicodeCharsCount(str) {
export function utilUnicodeCharsTruncated(str, limit) {
return Array.from(str).slice(0, limit).join('');
}
// Variation of d3.json (https://github.com/d3/d3-fetch/blob/master/src/json.js)
export function utilFetchJson(resourse, init) {
return fetch(resourse, init)
.then((response) => {
// fetch in PhantomJS tests may return ok=false and status=0 even if it's okay
if ((!response.ok && response.status !== 0) || !response.json) throw new Error(response.status + ' ' + response.statusText);
if (response.status === 204 || response.status === 205) return;
return response.json();
});
}
+4 -4
View File
@@ -719,10 +719,10 @@ export function validationCrossingWays(context) {
edges.forEach(function(edge) {
var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];
var closestNodeInfo = geoSphericalClosestNode(edgeNodes, loc);
// if there is already a point nearby, use that
if (closestNodeInfo.distance < mergeThresholdInMeters) {
nodesToMerge.push(closestNodeInfo.node.id);
var nearby = geoSphericalClosestNode(edgeNodes, loc);
// if there is already a suitable node nearby, use that
if (!nearby.node.hasInterestingTags() && nearby.distance < mergeThresholdInMeters) {
nodesToMerge.push(nearby.node.id);
// else add the new node to the way
} else {
graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);
+3 -1
View File
@@ -25,7 +25,9 @@ export function validationHelpRequest(context) {
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.fixme_tag.message', { feature: utilDisplayLabel(entity, context.graph()) }) : '';
return entity ? t.html('issues.fixme_tag.message', {
feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)
}) : '';
},
dynamicFixes: function() {
return [
+1 -1
View File
@@ -35,7 +35,7 @@ export function validationIncompatibleSource() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.incompatible_source.' + invalidSource.id + '.feature.message', {
feature: utilDisplayLabel(entity, context.graph())
feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)
}) : '';
},
reference: getReference(invalidSource.id),
+6 -6
View File
@@ -89,7 +89,7 @@ export function validationMismatchedGeometry() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.tag_suggests_area.message', {
feature: utilDisplayLabel(entity, 'area'),
feature: utilDisplayLabel(entity, 'area', true /* verbose */),
tag: utilTagText({ tags: tagSuggestingArea })
}) : '';
},
@@ -162,7 +162,7 @@ export function validationMismatchedGeometry() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.vertex_as_point.message', {
feature: utilDisplayLabel(entity, 'vertex')
feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)
}) : '';
},
reference: function showReference(selection) {
@@ -185,7 +185,7 @@ export function validationMismatchedGeometry() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.point_as_vertex.message', {
feature: utilDisplayLabel(entity, 'point')
feature: utilDisplayLabel(entity, 'point', true /* verbose */)
}) : '';
},
reference: function showReference(selection) {
@@ -264,7 +264,7 @@ export function validationMismatchedGeometry() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.' + referenceId + '.message', {
feature: utilDisplayLabel(entity, targetGeom)
feature: utilDisplayLabel(entity, targetGeom, true /* verbose */)
}) : '';
},
reference: function showReference(selection) {
@@ -322,7 +322,7 @@ export function validationMismatchedGeometry() {
extractOnClick = function(context) {
var entityId = this.issue.entityIds[0];
var action = actionExtract(entityId);
var action = actionExtract(entityId, context.projection);
context.perform(
action,
t('operations.extract.annotation', { n: 1 })
@@ -371,7 +371,7 @@ export function validationMismatchedGeometry() {
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.unclosed_multipolygon_part.message', {
feature: utilDisplayLabel(entity, context.graph())
feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)
}) : '';
},
reference: showReference,
+9 -8
View File
@@ -10,17 +10,18 @@ export function validationMissingTag(context) {
var type = 'missing_tag';
function hasDescriptiveTags(entity, graph) {
var keys = Object.keys(entity.tags)
var onlyAttributeKeys = ['description', 'name', 'note', 'start_date'];
var entityDescriptiveKeys = Object.keys(entity.tags)
.filter(function(k) {
if (k === 'area' || k === 'name') {
return false;
} else {
return osmIsInterestingTag(k);
}
if (k === 'area' || !osmIsInterestingTag(k)) return false;
return !onlyAttributeKeys.some(function(attributeKey) {
return k === attributeKey || k.indexOf(attributeKey + ':') === 0;
});
});
if (entity.type === 'relation' &&
keys.length === 1 &&
entityDescriptiveKeys.length === 1 &&
entity.tags.type === 'multipolygon') {
// this relation's only interesting tag just says its a multipolygon,
// which is not descriptive enough
@@ -30,7 +31,7 @@ export function validationMissingTag(context) {
return osmOldMultipolygonOuterMemberOfRelation(entity, graph);
}
return keys.length > 0;
return entityDescriptiveKeys.length > 0;
}
function isUnknownRoad(entity) {
+36 -103
View File
@@ -1,57 +1,26 @@
import { t } from '../core/localizer';
import { matcher } from 'name-suggestion-index';
import * as countryCoder from '@ideditor/country-coder';
import { presetManager } from '../presets';
import { fileFetcher } from '../core/file_fetcher';
import { actionChangePreset } from '../actions/change_preset';
import { actionChangeTags } from '../actions/change_tags';
import { actionUpgradeTags } from '../actions/upgrade_tags';
import { fileFetcher } from '../core';
import { presetManager } from '../presets';
import { services } from '../services';
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { utilDisplayLabel, utilTagDiff } from '../util';
import { utilDisplayLabel, utilHashcode, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
let _dataDeprecated;
let _nsi;
export function validationOutdatedTags() {
const type = 'outdated_tags';
const nsiKeys = ['amenity', 'shop', 'tourism', 'leisure', 'office'];
let _waitingForDeprecated = true;
let _dataDeprecated;
// A concern here in switching to async data means that `_dataDeprecated`
// and `_nsi` will not be available at first, so the data on early tiles
// may not have tags validated fully.
// initialize deprecated tags array
// fetch deprecated tags
fileFetcher.get('deprecated')
.then(d => _dataDeprecated = d)
.catch(() => { /* ignore */ });
fileFetcher.get('nsi_brands')
.then(d => {
_nsi = {
brands: d.brands,
matcher: matcher(),
wikidata: {},
wikipedia: {}
};
// initialize name-suggestion-index matcher
_nsi.matcher.buildMatchIndex(d.brands);
// index all known wikipedia and wikidata tags
Object.keys(d.brands).forEach(kvnd => {
const brand = d.brands[kvnd];
const wd = brand.tags['brand:wikidata'];
const wp = brand.tags['brand:wikipedia'];
if (wd) { _nsi.wikidata[wd] = kvnd; }
if (wp) { _nsi.wikipedia[wp] = kvnd; }
});
return _nsi;
})
.catch(() => { /* ignore */ });
.catch(() => { /* ignore */ })
.finally(() => _waitingForDeprecated = false);
function oldTagIssues(entity, graph) {
@@ -59,8 +28,9 @@ export function validationOutdatedTags() {
let preset = presetManager.match(entity, graph);
let subtype = 'deprecated_tags';
if (!preset) return [];
if (!entity.hasInterestingTags()) return [];
// upgrade preset..
// Upgrade preset, if a replacement is available..
if (preset.replacement) {
const newPreset = presetManager.item(preset.replacement);
graph = actionChangePreset(entity.id, preset, newPreset, true /* skip field defaults */)(graph);
@@ -68,7 +38,7 @@ export function validationOutdatedTags() {
preset = newPreset;
}
// upgrade tags..
// Upgrade deprecated tags..
if (_dataDeprecated) {
const deprecatedTags = entity.deprecatedTags(_dataDeprecated);
if (deprecatedTags.length) {
@@ -79,7 +49,7 @@ export function validationOutdatedTags() {
}
}
// add missing addTags..
// Add missing addTags from the detected preset
let newTags = Object.assign({}, entity.tags); // shallow copy
if (preset.tags !== preset.addTags) {
Object.keys(preset.addTags).forEach(k => {
@@ -93,67 +63,27 @@ export function validationOutdatedTags() {
});
}
if (_nsi) {
// Do `wikidata` or `wikipedia` identify this entity as a brand? #6416
// If so, these tags can be swapped to `brand:wikidata`/`brand:wikipedia`
let isBrand;
if (newTags.wikidata) { // try matching `wikidata`
isBrand = _nsi.wikidata[newTags.wikidata];
}
if (!isBrand && newTags.wikipedia) { // fallback to `wikipedia`
isBrand = _nsi.wikipedia[newTags.wikipedia];
}
if (isBrand && !newTags.office) { // but avoid doing this for corporate offices
if (newTags.wikidata) {
newTags['brand:wikidata'] = newTags.wikidata;
delete newTags.wikidata;
}
if (newTags.wikipedia) {
newTags['brand:wikipedia'] = newTags.wikipedia;
delete newTags.wikipedia;
}
// I considered setting `name` and other tags here, but they aren't unique per wikidata
// (Q2759586 -> in USA "Papa John's", in Russia "Папа Джонс")
// So users will really need to use a preset or assign `name` themselves.
}
// try key/value|name match against name-suggestion-index
if (newTags.name) {
for (let i = 0; i < nsiKeys.length; i++) {
const k = nsiKeys[i];
if (!newTags[k]) continue;
const center = entity.extent(graph).center();
const countryCode = countryCoder.iso1A2Code(center);
const match = _nsi.matcher.matchKVN(k, newTags[k], newTags.name, countryCode && countryCode.toLowerCase());
if (!match) continue;
// for now skip ambiguous matches (like Target~(USA) vs Target~(Australia))
if (match.d) continue;
const brand = _nsi.brands[match.kvnd];
if (brand && brand.tags['brand:wikidata'] &&
brand.tags['brand:wikidata'] !== entity.tags['not:brand:wikidata']) {
subtype = 'noncanonical_brand';
const keepTags = ['takeaway'].reduce((acc, k) => {
if (newTags[k]) {
acc[k] = newTags[k];
}
return acc;
}, {});
nsiKeys.forEach(k => delete newTags[k]);
Object.assign(newTags, brand.tags, keepTags);
break;
}
// Attempt to match a canonical record in the name-suggestion-index.
const nsi = services.nsi;
let waitingForNsi = false;
if (nsi) {
waitingForNsi = (nsi.status() === 'loading');
if (!waitingForNsi) {
const loc = entity.extent(graph).center();
const result = nsi.upgradeTags(newTags, loc);
if (result) {
newTags = result;
subtype = 'noncanonical_brand';
}
}
}
let issues = [];
issues.provisional = (_waitingForDeprecated || waitingForNsi);
// determine diff
const tagDiff = utilTagDiff(oldTags, newTags);
if (!tagDiff.length) return [];
if (!tagDiff.length) return issues;
const isOnlyAddingTags = tagDiff.every(d => d.type === '+');
@@ -168,14 +98,14 @@ export function validationOutdatedTags() {
// don't allow autofixing brand tags
let autoArgs = subtype !== 'noncanonical_brand' ? [doUpgrade, t('issues.fix.upgrade_tags.annotation')] : null;
return [new validationIssue({
issues.push(new validationIssue({
type: type,
subtype: subtype,
severity: 'warning',
message: showMessage,
reference: showReference,
entityIds: [entity.id],
hash: JSON.stringify(tagDiff),
hash: utilHashcode(JSON.stringify(tagDiff)),
dynamicFixes: () => {
return [
new validationIssueFix({
@@ -187,7 +117,8 @@ export function validationOutdatedTags() {
})
];
}
})];
}));
return issues;
function doUpgrade(graph) {
@@ -215,7 +146,9 @@ export function validationOutdatedTags() {
if (subtype === 'noncanonical_brand' && isOnlyAddingTags) {
messageID += '_incomplete';
}
return t.html(messageID, { feature: utilDisplayLabel(currEntity, context.graph()) });
return t.html(messageID, {
feature: utilDisplayLabel(currEntity, context.graph(), true /* verbose */)
});
}
@@ -302,7 +235,7 @@ export function validationOutdatedTags() {
if (!currMultipolygon) return '';
return t.html('issues.old_multipolygon.message',
{ multipolygon: utilDisplayLabel(currMultipolygon, context.graph()) }
{ multipolygon: utilDisplayLabel(currMultipolygon, context.graph(), true /* verbose */) }
);
}
+30 -29
View File
@@ -1,36 +1,33 @@
import { fileFetcher } from '../core/file_fetcher';
import { t, localizer } from '../core/localizer';
import { presetManager } from '../presets';
import { validationIssue, validationIssueFix } from '../core/validation';
import { actionChangeTags } from '../actions/change_tags';
import { presetManager } from '../presets';
import { services } from '../services';
import { t, localizer } from '../core/localizer';
import { validationIssue, validationIssueFix } from '../core/validation';
let _discardNameRegexes = [];
export function validationSuspiciousName() {
const type = 'suspicious_name';
const keysToTestForGenericValues = [
'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',
'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'
];
// A concern here in switching to async data means that `_nsiFilters` will not
// be available at first, so the data on early tiles may not have tags validated fully.
fileFetcher.get('nsi_filters')
.then(filters => {
// known list of generic names (e.g. "bar")
_discardNameRegexes = filters.discardNames
.map(discardName => new RegExp(discardName, 'i'));
})
.catch(() => { /* ignore */ });
let _waitingForNsi = false;
function isDiscardedSuggestionName(lowercaseName) {
return _discardNameRegexes.some(regex => regex.test(lowercaseName));
// Attempt to match a generic record in the name-suggestion-index.
function isGenericMatchInNsi(tags) {
const nsi = services.nsi;
if (nsi) {
_waitingForNsi = (nsi.status() === 'loading');
if (!_waitingForNsi) {
return nsi.isGenericName(tags);
}
}
return false;
}
// test if the name is just the key or tag value (e.g. "park")
// Test if the name is just the key or tag value (e.g. "park")
function nameMatchesRawTag(lowercaseName, tags) {
for (let i = 0; i < keysToTestForGenericValues.length; i++) {
let key = keysToTestForGenericValues[i];
@@ -50,7 +47,7 @@ export function validationSuspiciousName() {
function isGenericName(name, tags) {
name = name.toLowerCase();
return nameMatchesRawTag(name, tags) || isDiscardedSuggestionName(name);
return nameMatchesRawTag(name, tags) || isGenericMatchInNsi(tags);
}
function makeGenericNameIssue(entityId, nameKey, genericName, langCode) {
@@ -69,7 +66,7 @@ export function validationSuspiciousName() {
},
reference: showReference,
entityIds: [entityId],
hash: nameKey + '=' + genericName,
hash: `${nameKey}=${genericName}`,
dynamicFixes: function() {
return [
new validationIssueFix({
@@ -115,7 +112,7 @@ export function validationSuspiciousName() {
},
reference: showReference,
entityIds: [entityId],
hash: nameKey + '=' + incorrectName,
hash: `${nameKey}=${incorrectName}`,
dynamicFixes: function() {
return [
new validationIssueFix({
@@ -147,18 +144,21 @@ export function validationSuspiciousName() {
let validation = function checkGenericName(entity) {
// a generic name is okay if it's a known brand or entity
if (entity.hasWikidata()) return [];
const tags = entity.tags;
// a generic name is allowed if it's a known brand or entity
const hasWikidata = (!!tags.wikidata || !!tags['brand:wikidata'] || !!tags['operator:wikidata']);
if (hasWikidata) return [];
let issues = [];
const notNames = (entity.tags['not:name'] || '').split(';');
const notNames = (tags['not:name'] || '').split(';');
for (let key in entity.tags) {
for (let key in tags) {
const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);
if (!m) continue;
const langCode = m.length >= 2 ? m[1] : null;
const value = entity.tags[key];
const value = tags[key];
if (notNames.length) {
for (let i in notNames) {
const notName = notNames[i];
@@ -168,7 +168,8 @@ export function validationSuspiciousName() {
}
}
}
if (isGenericName(value, entity.tags)) {
if (isGenericName(value, tags)) {
issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading
issues.push(makeGenericNameIssue(entity.id, key, value, langCode));
}
}
+4 -2
View File
@@ -76,11 +76,13 @@ export function validationUnsquareWay(context) {
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.unsquare_way.message', { feature: utilDisplayLabel(entity, context.graph()) }) : '';
return entity ? t.html('issues.unsquare_way.message', {
feature: utilDisplayLabel(entity, context.graph())
}) : '';
},
reference: showReference,
entityIds: [entity.id],
hash: JSON.stringify(autoArgs !== undefined) + degreeThreshold,
hash: degreeThreshold,
dynamicFixes: function() {
return [
new validationIssueFix({