mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-15 21:48:20 +02:00
Merge branch 'develop' into validation_queryparams_conflict_resolve
This commit is contained in:
@@ -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}));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
+362
-483
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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] : []);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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] : []);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */) }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user