mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-23 00:29:50 +02:00
Merge branch 'develop' into validation_queryparams_conflict_resolve
This commit is contained in:
@@ -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
Reference in New Issue
Block a user