mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-22 16:19:48 +02:00
Merge branch 'develop' into vegbilder
This commit is contained in:
@@ -3,6 +3,7 @@ export function actionChangePreset(entityID, oldPreset, newPreset, skipFieldDefa
|
||||
var entity = graph.entity(entityID);
|
||||
var geometry = entity.geometry(graph);
|
||||
var tags = entity.tags;
|
||||
const loc = entity.extent(graph).center();
|
||||
|
||||
// preserve tags that the new preset might care about, if any
|
||||
var preserveKeys;
|
||||
@@ -15,14 +16,14 @@ export function actionChangePreset(entityID, oldPreset, newPreset, skipFieldDefa
|
||||
// only if old preset is not a sub-preset of the new one:
|
||||
// preserve tags for which the new preset has a field
|
||||
// https://github.com/openstreetmap/iD/issues/9372
|
||||
newPreset.fields().concat(newPreset.moreFields())
|
||||
newPreset.fields(loc).concat(newPreset.moreFields(loc))
|
||||
.filter(f => f.matchGeometry(geometry))
|
||||
.map(f => f.key).filter(Boolean)
|
||||
.forEach(key => preserveKeys.push(key));
|
||||
}
|
||||
}
|
||||
if (oldPreset) tags = oldPreset.unsetTags(tags, geometry, preserveKeys);
|
||||
if (newPreset) tags = newPreset.setTags(tags, geometry, skipFieldDefaults);
|
||||
if (oldPreset) tags = oldPreset.unsetTags(tags, geometry, preserveKeys, false, loc);
|
||||
if (newPreset) tags = newPreset.setTags(tags, geometry, skipFieldDefaults, loc);
|
||||
|
||||
return graph.replace(entity.update({tags: tags}));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LocationConflation from '@ideditor/location-conflation';
|
||||
import LocationConflation from '@rapideditor/location-conflation';
|
||||
import whichPolygon from 'which-polygon';
|
||||
import calcArea from '@mapbox/geojson-area';
|
||||
|
||||
@@ -16,8 +16,8 @@ const _loco = new LocationConflation(); // instance of a location-conflation
|
||||
* };
|
||||
*
|
||||
* For more info see the location-conflation and country-coder projects, see:
|
||||
* https://github.com/ideditor/location-conflation
|
||||
* https://github.com/ideditor/country-coder
|
||||
* https://github.com/rapideditor/location-conflation
|
||||
* https://github.com/rapideditor/country-coder
|
||||
*/
|
||||
export class LocationManager {
|
||||
|
||||
|
||||
@@ -176,8 +176,17 @@ export function coreLocalizer() {
|
||||
|
||||
let locale = _localeCode;
|
||||
if (locale.toLowerCase() === 'en-us') locale = 'en';
|
||||
_languageNames = _localeStrings.general[locale].languageNames;
|
||||
_scriptNames = _localeStrings.general[locale].scriptNames;
|
||||
|
||||
// some locales (like fr-FR) have no languageNames or scriptNames,
|
||||
// so we need to load them from the base language (see #8673)
|
||||
_languageNames = (
|
||||
_localeStrings.general[locale].languageNames ||
|
||||
_localeStrings.general[_languageCode].languageNames
|
||||
);
|
||||
_scriptNames = (
|
||||
_localeStrings.general[locale].scriptNames ||
|
||||
_localeStrings.general[_languageCode].scriptNames
|
||||
);
|
||||
|
||||
_usesMetric = _localeCode.slice(-3).toLowerCase() !== '-us';
|
||||
}
|
||||
@@ -232,7 +241,7 @@ export function coreLocalizer() {
|
||||
* the given `stringId`. If no string can be found in the requested locale,
|
||||
* we'll recurse down all the `_localeCodes` until one is found.
|
||||
*
|
||||
* @param {string} stringId string identifier
|
||||
* @param {string} origStringId string identifier
|
||||
* @param {object?} replacements token replacements and default string
|
||||
* @param {string?} locale locale to use (defaults to currentLocale)
|
||||
* @return {string?} localized string
|
||||
@@ -392,7 +401,7 @@ export function coreLocalizer() {
|
||||
|
||||
localizer.languageName = (code, options) => {
|
||||
|
||||
if (_languageNames[code]) { // name in locale language
|
||||
if (_languageNames && _languageNames[code]) { // name in locale language
|
||||
// e.g. "German"
|
||||
return _languageNames[code];
|
||||
}
|
||||
@@ -409,9 +418,9 @@ export function coreLocalizer() {
|
||||
} else if (langInfo.base && langInfo.script) {
|
||||
const base = langInfo.base; // the code of the language this is based on
|
||||
|
||||
if (_languageNames[base]) { // base language name in locale language
|
||||
if (_languageNames && _languageNames[base]) { // base language name in locale language
|
||||
const scriptCode = langInfo.script;
|
||||
const script = _scriptNames[scriptCode] || scriptCode;
|
||||
const script = (_scriptNames && _scriptNames[scriptCode]) || scriptCode;
|
||||
// e.g. "Serbian (Cyrillic)"
|
||||
return localizer.t('translate.language_and_code', { language: _languageNames[base], code: script });
|
||||
|
||||
@@ -424,5 +433,81 @@ export function coreLocalizer() {
|
||||
return code; // if not found, use the code
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that formats a floating-point number in the given
|
||||
* locale.
|
||||
*/
|
||||
localizer.floatFormatter = (locale) => {
|
||||
if (!('Intl' in window && 'NumberFormat' in Intl &&
|
||||
'formatToParts' in Intl.NumberFormat.prototype)) {
|
||||
return (number, fractionDigits) => {
|
||||
return fractionDigits === undefined ? number.toString() : number.toFixed(fractionDigits);
|
||||
};
|
||||
} else {
|
||||
return (number, fractionDigits) => number.toLocaleString(locale, {
|
||||
minimumFractionDigits: fractionDigits,
|
||||
maximumFractionDigits: fractionDigits === undefined ? 20 : fractionDigits,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that parses a number formatted according to the given
|
||||
* locale as a floating-point number.
|
||||
*/
|
||||
localizer.floatParser = (locale) => {
|
||||
// https://stackoverflow.com/a/55366435/4585461
|
||||
const polyfill = (string) => +string.trim();
|
||||
if (!('Intl' in window && 'NumberFormat' in Intl)) return polyfill;
|
||||
const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 20 });
|
||||
if (!('formatToParts' in format)) return polyfill;
|
||||
const parts = format.formatToParts(-12345.6);
|
||||
const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
|
||||
const index = new Map(numerals.map((d, i) => [d, i]));
|
||||
const literalPart = parts.find(d => d.type === 'literal');
|
||||
const literal = literalPart && new RegExp(`[${literalPart.value}]`, 'g');
|
||||
const groupPart = parts.find(d => d.type === 'group');
|
||||
const group = groupPart && new RegExp(`[${groupPart.value}]`, 'g');
|
||||
const decimalPart = parts.find(d => d.type === 'decimal');
|
||||
const decimal = decimalPart && new RegExp(`[${decimalPart.value}]`);
|
||||
const numeral = new RegExp(`[${numerals.join('')}]`, 'g');
|
||||
const getIndex = d => index.get(d);
|
||||
return (string) => {
|
||||
string = string.trim();
|
||||
if (literal) string = string.replace(literal, '');
|
||||
if (group) string = string.replace(group, '');
|
||||
if (decimal) string = string.replace(decimal, '.');
|
||||
string = string.replace(numeral, getIndex);
|
||||
return string ? +string : NaN;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that returns the number of decimal places in a
|
||||
* formatted number string.
|
||||
*/
|
||||
localizer.decimalPlaceCounter = (locale) => {
|
||||
var literal, group, decimal;
|
||||
if ('Intl' in window && 'NumberFormat' in Intl) {
|
||||
const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 20 });
|
||||
if ('formatToParts' in format) {
|
||||
const parts = format.formatToParts(-12345.6);
|
||||
const literalPart = parts.find(d => d.type === 'literal');
|
||||
literal = literalPart && new RegExp(`[${literalPart.value}]`, 'g');
|
||||
const groupPart = parts.find(d => d.type === 'group');
|
||||
group = groupPart && new RegExp(`[${groupPart.value}]`, 'g');
|
||||
const decimalPart = parts.find(d => d.type === 'decimal');
|
||||
decimal = decimalPart && new RegExp(`[${decimalPart.value}]`);
|
||||
}
|
||||
}
|
||||
return (string) => {
|
||||
string = string.trim();
|
||||
if (literal) string = string.replace(literal, '');
|
||||
if (group) string = string.replace(group, '');
|
||||
const parts = string.split(decimal || '.');
|
||||
return parts && parts[1] && parts[1].length || 0;
|
||||
};
|
||||
};
|
||||
|
||||
return localizer;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ export function modeAddArea(context, mode) {
|
||||
.on('startFromWay', startFromWay)
|
||||
.on('startFromNode', startFromNode);
|
||||
|
||||
var defaultTags = { area: 'yes' };
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area');
|
||||
|
||||
function defaultTags(loc) {
|
||||
var defaultTags = { area: 'yes' };
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area', false, loc);
|
||||
return defaultTags;
|
||||
}
|
||||
|
||||
function actionClose(wayId) {
|
||||
return function (graph) {
|
||||
@@ -29,7 +31,7 @@ export function modeAddArea(context, mode) {
|
||||
function start(loc) {
|
||||
var startGraph = context.graph();
|
||||
var node = osmNode({ loc: loc });
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(node),
|
||||
@@ -45,7 +47,7 @@ export function modeAddArea(context, mode) {
|
||||
function startFromWay(loc, edge) {
|
||||
var startGraph = context.graph();
|
||||
var node = osmNode({ loc: loc });
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(node),
|
||||
@@ -61,7 +63,7 @@ export function modeAddArea(context, mode) {
|
||||
|
||||
function startFromNode(node) {
|
||||
var startGraph = context.graph();
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(node.loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(way),
|
||||
|
||||
@@ -15,14 +15,17 @@ export function modeAddLine(context, mode) {
|
||||
.on('startFromWay', startFromWay)
|
||||
.on('startFromNode', startFromNode);
|
||||
|
||||
var defaultTags = {};
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line');
|
||||
function defaultTags(loc) {
|
||||
var defaultTags = {};
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line', false, loc);
|
||||
return defaultTags;
|
||||
}
|
||||
|
||||
|
||||
function start(loc) {
|
||||
var startGraph = context.graph();
|
||||
var node = osmNode({ loc: loc });
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(node),
|
||||
@@ -37,7 +40,7 @@ export function modeAddLine(context, mode) {
|
||||
function startFromWay(loc, edge) {
|
||||
var startGraph = context.graph();
|
||||
var node = osmNode({ loc: loc });
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(node),
|
||||
@@ -52,7 +55,7 @@ export function modeAddLine(context, mode) {
|
||||
|
||||
function startFromNode(node) {
|
||||
var startGraph = context.graph();
|
||||
var way = osmWay({ tags: defaultTags });
|
||||
var way = osmWay({ tags: defaultTags(node.loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(way),
|
||||
|
||||
@@ -19,12 +19,15 @@ export function modeAddPoint(context, mode) {
|
||||
.on('cancel', cancel)
|
||||
.on('finish', cancel);
|
||||
|
||||
var defaultTags = {};
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point');
|
||||
function defaultTags(loc) {
|
||||
var defaultTags = {};
|
||||
if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point', false, loc);
|
||||
return defaultTags;
|
||||
}
|
||||
|
||||
|
||||
function add(loc) {
|
||||
var node = osmNode({ loc: loc, tags: defaultTags });
|
||||
var node = osmNode({ loc: loc, tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddEntity(node),
|
||||
@@ -36,7 +39,7 @@ export function modeAddPoint(context, mode) {
|
||||
|
||||
|
||||
function addWay(loc, edge) {
|
||||
var node = osmNode({ tags: defaultTags });
|
||||
var node = osmNode({ tags: defaultTags(loc) });
|
||||
|
||||
context.perform(
|
||||
actionAddMidpoint({loc: loc, edge: edge}, node),
|
||||
@@ -54,14 +57,15 @@ export function modeAddPoint(context, mode) {
|
||||
|
||||
|
||||
function addNode(node) {
|
||||
if (Object.keys(defaultTags).length === 0) {
|
||||
const _defaultTags = defaultTags(node.loc);
|
||||
if (Object.keys(_defaultTags).length === 0) {
|
||||
enterSelectMode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
var tags = Object.assign({}, node.tags); // shallow copy
|
||||
for (var key in defaultTags) {
|
||||
tags[key] = defaultTags[key];
|
||||
for (var key in _defaultTags) {
|
||||
tags[key] = _defaultTags[key];
|
||||
}
|
||||
|
||||
context.perform(
|
||||
|
||||
+12
-4
@@ -76,11 +76,11 @@ export function osmTagSuggestingArea(tags) {
|
||||
var returnTags = {};
|
||||
for (var realKey in tags) {
|
||||
const key = osmRemoveLifecyclePrefix(realKey);
|
||||
if (key in osmAreaKeys && !(tags[key] in osmAreaKeys[key])) {
|
||||
if (key in osmAreaKeys && !(tags[realKey] in osmAreaKeys[key])) {
|
||||
returnTags[realKey] = tags[realKey];
|
||||
return returnTags;
|
||||
}
|
||||
if (key in osmAreaKeysExceptions && tags[key] in osmAreaKeysExceptions[key]) {
|
||||
if (key in osmAreaKeysExceptions && tags[realKey] in osmAreaKeysExceptions[key]) {
|
||||
returnTags[realKey] = tags[realKey];
|
||||
return returnTags;
|
||||
}
|
||||
@@ -152,6 +152,8 @@ export var osmOneWayTags = {
|
||||
'yes': true
|
||||
},
|
||||
'seamark:type': {
|
||||
'two-way_route': true,
|
||||
'recommended_traffic_lane': true,
|
||||
'separation_lane': true,
|
||||
'separation_roundabout': true
|
||||
},
|
||||
@@ -160,7 +162,9 @@ export var osmOneWayTags = {
|
||||
'ditch': true,
|
||||
'drain': true,
|
||||
'fish_pass': true,
|
||||
'pressurised': true,
|
||||
'river': true,
|
||||
'spillway': true,
|
||||
'stream': true,
|
||||
'tidal_channel': true
|
||||
}
|
||||
@@ -197,7 +201,7 @@ export var osmSemipavedTags = {
|
||||
export var osmRightSideIsInsideTags = {
|
||||
'natural': {
|
||||
'cliff': true,
|
||||
'coastline': 'coastline',
|
||||
'coastline': 'coastline'
|
||||
},
|
||||
'barrier': {
|
||||
'retaining_wall': true,
|
||||
@@ -206,7 +210,8 @@ export var osmRightSideIsInsideTags = {
|
||||
'city_wall': true,
|
||||
},
|
||||
'man_made': {
|
||||
'embankment': true
|
||||
'embankment': true,
|
||||
'quay': true
|
||||
},
|
||||
'waterway': {
|
||||
'weir': true
|
||||
@@ -237,3 +242,6 @@ export var osmRailwayTrackTagValues = {
|
||||
export var osmFlowingWaterwayTagValues = {
|
||||
canal: true, ditch: true, drain: true, fish_pass: true, river: true, stream: true, tidal_channel: true
|
||||
};
|
||||
|
||||
// Tags which values should be considered case sensitive when offering tag suggestions
|
||||
export const allowUpperCaseTagValues = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times|_ref|manufacturer|country|target|brewery|cai_scale|traffic_sign/;
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ import { geoArea as d3_geoArea } from 'd3-geo';
|
||||
import { geoExtent, geoVecCross } from '../geo';
|
||||
import { osmEntity } from './entity';
|
||||
import { osmLanes } from './lanes';
|
||||
import { osmTagSuggestingArea, osmOneWayTags, osmRightSideIsInsideTags } from './tags';
|
||||
import { osmTagSuggestingArea, osmOneWayTags, osmRightSideIsInsideTags, osmRemoveLifecyclePrefix } from './tags';
|
||||
import { utilArrayUniq } from '../util';
|
||||
|
||||
|
||||
@@ -167,8 +167,9 @@ Object.assign(osmWay.prototype, {
|
||||
// i.e. the right side is the 'inside' (e.g. the right side of a
|
||||
// natural=cliff is lower).
|
||||
sidednessIdentifier: function() {
|
||||
for (var key in this.tags) {
|
||||
var value = this.tags[key];
|
||||
for (const realKey in this.tags) {
|
||||
const value = this.tags[realKey];
|
||||
const key = osmRemoveLifecyclePrefix(realKey);
|
||||
if (key in osmRightSideIsInsideTags && (value in osmRightSideIsInsideTags[key])) {
|
||||
if (osmRightSideIsInsideTags[key][value] === true) {
|
||||
return key;
|
||||
|
||||
@@ -163,9 +163,6 @@ export function presetIndex() {
|
||||
// Rebuild universal fields array
|
||||
_universal = Object.values(_fields).filter(field => field.universal);
|
||||
|
||||
// Reset all the preset fields - they'll need to be resolved again
|
||||
Object.values(_presets).forEach(preset => preset.resetFields());
|
||||
|
||||
// Rebuild geometry index
|
||||
_geometryIndex = { point: {}, vertex: {}, line: {}, area: {}, relation: {} };
|
||||
_this.collection.forEach(preset => {
|
||||
|
||||
+25
-15
@@ -1,7 +1,10 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { t } from '../core/localizer';
|
||||
import { osmAreaKeys, osmAreaKeysExceptions } from '../osm/tags';
|
||||
import { utilArrayUniq, utilObjectOmit } from '../util';
|
||||
import { utilSafeClassName } from '../util/util';
|
||||
import { locationManager } from '../core/LocationManager';
|
||||
|
||||
|
||||
//
|
||||
@@ -13,8 +16,6 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
allPresets = allPresets || {};
|
||||
let _this = Object.assign({}, preset); // shallow copy
|
||||
let _addable = addable || false;
|
||||
let _resolvedFields; // cache
|
||||
let _resolvedMoreFields; // cache
|
||||
let _searchName; // cache
|
||||
let _searchNameStripped; // cache
|
||||
let _searchAliases; // cache
|
||||
@@ -40,11 +41,9 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
|
||||
_this.originalMoreFields = (_this.moreFields || []);
|
||||
|
||||
_this.fields = () => _resolvedFields || (_resolvedFields = resolveFields('fields'));
|
||||
_this.fields = loc => resolveFields('fields', loc);
|
||||
|
||||
_this.moreFields = () => _resolvedMoreFields || (_resolvedMoreFields = resolveFields('moreFields'));
|
||||
|
||||
_this.resetFields = () => _resolvedFields = _resolvedMoreFields = null;
|
||||
_this.moreFields = loc => resolveFields('moreFields', loc);
|
||||
|
||||
_this.tags = _this.tags || {};
|
||||
|
||||
@@ -219,13 +218,13 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
};
|
||||
|
||||
|
||||
_this.unsetTags = (tags, geometry, ignoringKeys, skipFieldDefaults) => {
|
||||
_this.unsetTags = (tags, geometry, ignoringKeys, skipFieldDefaults, loc) => {
|
||||
// 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 => {
|
||||
_this.fields(loc).forEach(field => {
|
||||
if (field.matchGeometry(geometry) && field.key &&
|
||||
field.default === tags[field.key] &&
|
||||
(!ignoringKeys || ignoringKeys.indexOf(field.key) === -1)) {
|
||||
@@ -239,7 +238,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
};
|
||||
|
||||
|
||||
_this.setTags = (tags, geometry, skipFieldDefaults) => {
|
||||
_this.setTags = (tags, geometry, skipFieldDefaults, loc) => {
|
||||
const addTags = _this.addTags;
|
||||
tags = Object.assign({}, tags); // shallow copy
|
||||
|
||||
@@ -277,7 +276,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
}
|
||||
|
||||
if (geometry && !skipFieldDefaults) {
|
||||
_this.fields().forEach(field => {
|
||||
_this.fields(loc).forEach(field => {
|
||||
if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) {
|
||||
tags[field.key] = field.default;
|
||||
}
|
||||
@@ -290,14 +289,14 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
|
||||
// For a preset without fields, use the fields of the parent preset.
|
||||
// Replace {preset} placeholders with the fields of the specified presets.
|
||||
function resolveFields(which) {
|
||||
function resolveFields(which, loc) {
|
||||
const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields);
|
||||
let resolved = [];
|
||||
|
||||
fieldIDs.forEach(fieldID => {
|
||||
const match = fieldID.match(referenceRegex);
|
||||
if (match !== null) { // a presetID wrapped in braces {}
|
||||
resolved = resolved.concat(inheritFields(match[1], which));
|
||||
resolved = resolved.concat(inheritFields(allPresets[match[1]], which));
|
||||
} else if (allFields[fieldID]) { // a normal fieldID
|
||||
resolved.push(allFields[fieldID]);
|
||||
} else {
|
||||
@@ -310,7 +309,19 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
const endIndex = _this.id.lastIndexOf('/');
|
||||
const parentID = endIndex && _this.id.substring(0, endIndex);
|
||||
if (parentID) {
|
||||
resolved = inheritFields(parentID, which);
|
||||
let parent = allPresets[parentID];
|
||||
if (loc) {
|
||||
const validHere = locationManager.locationSetsAt(loc);
|
||||
if (parent?.locationSetID && !validHere[parent.locationSetID]) {
|
||||
// this is a preset for which a regional variant of the main preset exists
|
||||
const candidateIDs = Object.keys(allPresets).filter(k => k.startsWith(parentID));
|
||||
parent = allPresets[candidateIDs.find(candidateID => {
|
||||
const candidate = allPresets[candidateID];
|
||||
return validHere[candidate.locationSetID] && isEqual(candidate.tags, parent.tags);
|
||||
})];
|
||||
}
|
||||
}
|
||||
resolved = inheritFields(parent, which);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +329,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
||||
|
||||
|
||||
// returns an array of fields to inherit from the given presetID, if found
|
||||
function inheritFields(presetID, which) {
|
||||
const parent = allPresets[presetID];
|
||||
function inheritFields(parent, which) {
|
||||
if (!parent) return [];
|
||||
|
||||
if (which === 'fields') {
|
||||
|
||||
@@ -300,11 +300,9 @@ export default {
|
||||
|
||||
// Register viewer resize handler
|
||||
context.ui().photoviewer.on('resize.kartaview', function(dimensions) {
|
||||
imgZoom = d3_zoom()
|
||||
imgZoom
|
||||
.extent([[0, 0], dimensions])
|
||||
.translateExtent([[0, 0], dimensions])
|
||||
.scaleExtent([1, 15])
|
||||
.on('zoom', zoomPan);
|
||||
.translateExtent([[0, 0], dimensions]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { json as d3_json } from 'd3-fetch';
|
||||
import RBush from 'rbush';
|
||||
import { geoExtent } from '../geo';
|
||||
import { utilQsString } from '../util';
|
||||
import { localizer } from '../core';
|
||||
|
||||
import { nominatimApiUrl } from '../../config/id.js';
|
||||
|
||||
@@ -56,7 +57,12 @@ export default {
|
||||
var controller = new AbortController();
|
||||
_inflight[url] = controller;
|
||||
|
||||
d3_json(url, { signal: controller.signal })
|
||||
d3_json(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept-Language': localizer.localeCodes().join(',')
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
delete _inflight[url];
|
||||
if (result && result.error) {
|
||||
@@ -82,7 +88,12 @@ export default {
|
||||
var controller = new AbortController();
|
||||
_inflight[url] = controller;
|
||||
|
||||
d3_json(url, { signal: controller.signal })
|
||||
d3_json(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept-Language': localizer.localeCodes().join(',')
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
delete _inflight[url];
|
||||
if (result && result.error) {
|
||||
|
||||
@@ -852,7 +852,7 @@ export default {
|
||||
bubbleIdQuadKey = '0' + bubbleIdQuadKey;
|
||||
}
|
||||
const imgUrlPrefix = streetsideImagesApi + 'hs' + bubbleIdQuadKey;
|
||||
const imgUrlSuffix = '.jpg?g=6338&n=z';
|
||||
const imgUrlSuffix = '.jpg?g=13515&n=z';
|
||||
|
||||
// Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12
|
||||
const faceKeys = ['01','02','03','10','11','12'];
|
||||
|
||||
@@ -4,6 +4,7 @@ import { json as d3_json } from 'd3-fetch';
|
||||
|
||||
import { utilObjectOmit, utilQsString } from '../util';
|
||||
import { localizer } from '../core/localizer';
|
||||
import { allowUpperCaseTagValues } from '../osm/tags';
|
||||
|
||||
import { taginfoApiUrl } from '../../config/id.js';
|
||||
|
||||
@@ -312,8 +313,7 @@ export default {
|
||||
// A few OSM keys expect values to contain uppercase values (see #3377).
|
||||
// This is not an exhaustive list (e.g. `name` also has uppercase values)
|
||||
// but these are the fields where taginfo value lookup is most useful.
|
||||
var re = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times|_ref|manufacturer|country|target|brewery|cai_scale/;
|
||||
var allowUpperCase = re.test(params.key);
|
||||
var allowUpperCase = allowUpperCaseTagValues.test(params.key);
|
||||
var f = filterValues(allowUpperCase);
|
||||
|
||||
var result = d.data.filter(f).map(valKeyDescription);
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
|
||||
|
||||
// Search for Wikidata items matching the query
|
||||
itemsForSearchQuery: function(query, callback) {
|
||||
itemsForSearchQuery: function(query, callback, language) {
|
||||
if (!query) {
|
||||
if (callback) callback('No query', {});
|
||||
return;
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
search: query,
|
||||
type: 'item',
|
||||
// the language to search
|
||||
language: lang,
|
||||
language: language || lang,
|
||||
// the language for the label and description in the result
|
||||
uselang: lang,
|
||||
limit: 10,
|
||||
@@ -40,9 +40,17 @@ export default {
|
||||
});
|
||||
|
||||
d3_json(url)
|
||||
.then(function(result) {
|
||||
.then(result => {
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
if (result.error.code === 'badvalue' &&
|
||||
result.error.info.includes(lang) &&
|
||||
!language && lang.includes('-')) {
|
||||
// retry without "country suffix" region subtag
|
||||
this.itemsForSearchQuery(query, callback, lang.split('-')[0]);
|
||||
return;
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
}
|
||||
if (callback) callback(null, result.search || {});
|
||||
})
|
||||
|
||||
@@ -159,7 +159,12 @@ export function svgTagClasses() {
|
||||
classes.push('tag-wikidata');
|
||||
}
|
||||
|
||||
return classes.join(' ').trim();
|
||||
// ensure that classes for tags keys/values with special characters like spaces
|
||||
// are not added to the DOM, because it can cause bizarre issues (#9448)
|
||||
return classes
|
||||
.filter(klass => /^[-_a-z0-9]+$/.test(klass))
|
||||
.join(' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ export function uiFeatureList(context) {
|
||||
}
|
||||
|
||||
// A location search takes priority over an ID search
|
||||
var idMatch = !locationMatch && q.match(/(?:^|\W)(node|way|relation|[nwr])\W?0*([1-9]\d*)(?:\W|$)/i);
|
||||
var idMatch = !locationMatch && q.match(/(?:^|\W)(node|way|relation|[nwr])\W{0,2}0*([1-9]\d*)(?:\W|$)/i);
|
||||
|
||||
if (idMatch) {
|
||||
var elemType = idMatch[1].charAt(0);
|
||||
|
||||
+147
-79
@@ -1,6 +1,6 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { presetManager } from '../../presets';
|
||||
import { fileFetcher } from '../../core/file_fetcher';
|
||||
@@ -36,91 +36,97 @@ export function uiFieldAddress(field, context) {
|
||||
.catch(function() { /* ignore */ });
|
||||
|
||||
|
||||
function getNearStreets() {
|
||||
function getNear(isAddressable, type, searchRadius, resultProp) {
|
||||
var extent = combinedEntityExtent();
|
||||
var l = extent.center();
|
||||
var box = geoExtent(l).padByMeters(200);
|
||||
var box = geoExtent(l).padByMeters(searchRadius);
|
||||
|
||||
var streets = context.history().intersects(box)
|
||||
var features = context.history().intersects(box)
|
||||
.filter(isAddressable)
|
||||
.map(function(d) {
|
||||
var loc = context.projection([
|
||||
(extent[0][0] + extent[1][0]) / 2,
|
||||
(extent[0][1] + extent[1][1]) / 2
|
||||
]);
|
||||
var choice = geoChooseEdge(context.graph().childNodes(d), loc, context.projection);
|
||||
.map(d => {
|
||||
let dist = geoSphericalDistance(d.extent(context.graph()).center(), l);
|
||||
|
||||
if (d.geometry(context.graph()) === 'line') {
|
||||
var loc = context.projection([
|
||||
(extent[0][0] + extent[1][0]) / 2,
|
||||
(extent[0][1] + extent[1][1]) / 2
|
||||
]);
|
||||
var choice = geoChooseEdge(context.graph().childNodes(d), loc, context.projection);
|
||||
dist = geoSphericalDistance(choice.loc, l);
|
||||
}
|
||||
|
||||
const value = resultProp && d.tags[resultProp] ? d.tags[resultProp] : d.tags.name;
|
||||
let title = value;
|
||||
if (type === 'street') {
|
||||
title = `${addrField.t('placeholders.street')}: ${title}`;
|
||||
} else if (type === 'place') {
|
||||
title = `${addrField.t('placeholders.place')}: ${title}`;
|
||||
}
|
||||
return {
|
||||
title: d.tags.name,
|
||||
value: d.tags.name,
|
||||
dist: choice.distance
|
||||
title,
|
||||
value,
|
||||
dist,
|
||||
type,
|
||||
klass: `address-${type}`
|
||||
};
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
return a.dist - b.dist;
|
||||
});
|
||||
|
||||
return utilArrayUniqBy(streets, 'value');
|
||||
return utilArrayUniqBy(features, 'value');
|
||||
}
|
||||
|
||||
function getNearStreets() {
|
||||
function isAddressable(d) {
|
||||
return d.tags.highway && d.tags.name && d.type === 'way';
|
||||
}
|
||||
|
||||
return getNear(isAddressable, 'street', 200);
|
||||
}
|
||||
|
||||
|
||||
function getNearCities() {
|
||||
var extent = combinedEntityExtent();
|
||||
var l = extent.center();
|
||||
var box = geoExtent(l).padByMeters(200);
|
||||
|
||||
var cities = context.history().intersects(box)
|
||||
.filter(isAddressable)
|
||||
.map(function(d) {
|
||||
return {
|
||||
title: d.tags['addr:city'] || d.tags.name,
|
||||
value: d.tags['addr:city'] || d.tags.name,
|
||||
dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
|
||||
};
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
return a.dist - b.dist;
|
||||
});
|
||||
|
||||
return utilArrayUniqBy(cities, 'value');
|
||||
|
||||
|
||||
function getNearPlaces() {
|
||||
function isAddressable(d) {
|
||||
if (d.tags.name) {
|
||||
if (d.tags.admin_level === '8' && d.tags.boundary === 'administrative') return true;
|
||||
if (d.tags.place) return true;
|
||||
if (d.tags.boundary === 'administrative' && d.tags.admin_level > 8) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return getNear(isAddressable, 'place', 200);
|
||||
}
|
||||
|
||||
function getNearCities() {
|
||||
function isAddressable(d) {
|
||||
if (d.tags.name) {
|
||||
if (d.tags.boundary === 'administrative' && d.tags.admin_level === '8') return true;
|
||||
if (d.tags.border_type === 'city') return true;
|
||||
if (d.tags.place === 'city' || d.tags.place === 'town' || d.tags.place === 'village') return true;
|
||||
}
|
||||
|
||||
if (d.tags['addr:city']) return true;
|
||||
if (d.tags[`${field.key}:city`]) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return getNear(isAddressable, 'city', 200, `${field.key}:city`);
|
||||
}
|
||||
|
||||
function getNearPostcodes() {
|
||||
return [... new Set([]
|
||||
.concat(getNearValues('postcode'))
|
||||
.concat(getNear(d => d.tags.postal_code, 'postcode', 200, 'postal_code')))];
|
||||
}
|
||||
|
||||
function getNearValues(key) {
|
||||
var extent = combinedEntityExtent();
|
||||
var l = extent.center();
|
||||
var box = geoExtent(l).padByMeters(200);
|
||||
const tagKey = `${field.key}:${key}`;
|
||||
|
||||
var results = context.history().intersects(box)
|
||||
.filter(function hasTag(d) { return _entityIDs.indexOf(d.id) === -1 && d.tags[key]; })
|
||||
.map(function(d) {
|
||||
return {
|
||||
title: d.tags[key],
|
||||
value: d.tags[key],
|
||||
dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
|
||||
};
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
return a.dist - b.dist;
|
||||
});
|
||||
function hasTag(d) {
|
||||
return _entityIDs.indexOf(d.id) === -1 && d.tags[tagKey];
|
||||
}
|
||||
|
||||
return utilArrayUniqBy(results, 'value');
|
||||
return getNear(hasTag, key, 200, tagKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,11 +148,11 @@ export function uiFieldAddress(field, context) {
|
||||
var dropdowns = addressFormat.dropdowns || [
|
||||
'city', 'county', 'country', 'district', 'hamlet',
|
||||
'neighbourhood', 'place', 'postcode', 'province',
|
||||
'quarter', 'state', 'street', 'subdistrict', 'suburb'
|
||||
'quarter', 'state', 'street', 'street+place', 'subdistrict', 'suburb'
|
||||
];
|
||||
|
||||
var widths = addressFormat.widths || {
|
||||
housenumber: 1/3, street: 2/3,
|
||||
housenumber: 1/5, unit: 1/5, street: 1/2, place: 1/2,
|
||||
city: 2/3, state: 1/4, postcode: 1/3
|
||||
};
|
||||
|
||||
@@ -191,16 +197,45 @@ export function uiFieldAddress(field, context) {
|
||||
function addDropdown(d) {
|
||||
if (dropdowns.indexOf(d.id) === -1) return; // not a dropdown
|
||||
|
||||
var nearValues = (d.id === 'street') ? getNearStreets
|
||||
: (d.id === 'city') ? getNearCities
|
||||
: getNearValues;
|
||||
var nearValues;
|
||||
switch (d.id) {
|
||||
case 'street':
|
||||
nearValues = getNearStreets;
|
||||
break;
|
||||
case 'place':
|
||||
nearValues = getNearPlaces;
|
||||
break;
|
||||
case 'street+place':
|
||||
nearValues = () => []
|
||||
.concat(getNearStreets())
|
||||
.concat(getNearPlaces());
|
||||
d.isAutoStreetPlace = true;
|
||||
d.id = _tags[`${field.key}:place`] ? 'place' : 'street';
|
||||
break;
|
||||
case 'city':
|
||||
nearValues = getNearCities;
|
||||
break;
|
||||
case 'postcode':
|
||||
nearValues = getNearPostcodes;
|
||||
break;
|
||||
default:
|
||||
nearValues = getNearValues;
|
||||
}
|
||||
|
||||
d3_select(this)
|
||||
.call(uiCombobox(context, 'address-' + d.id)
|
||||
.call(uiCombobox(context, `address-${d.isAutoStreetPlace ? 'street-place' : d.id}`)
|
||||
.minItems(1)
|
||||
.caseSensitive(true)
|
||||
.fetcher(function(value, callback) {
|
||||
callback(nearValues('addr:' + d.id));
|
||||
.fetcher(function(typedValue, callback) {
|
||||
typedValue = typedValue.toLowerCase();
|
||||
callback(nearValues(d.id)
|
||||
.filter(v => v.value.toLowerCase().indexOf(typedValue) !== -1));
|
||||
})
|
||||
.on('accept', function(selected) {
|
||||
if (d.isAutoStreetPlace) {
|
||||
// set subtag depending on selected entry
|
||||
d.id = selected ? selected.type : 'street';
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -248,42 +283,75 @@ export function uiFieldAddress(field, context) {
|
||||
|
||||
function change(onInput) {
|
||||
return function() {
|
||||
var tags = {};
|
||||
setTimeout(() => {
|
||||
var tags = {};
|
||||
|
||||
_wrap.selectAll('input')
|
||||
.each(function (subfield) {
|
||||
var key = field.key + ':' + subfield.id;
|
||||
_wrap.selectAll('input')
|
||||
.each(function (subfield) {
|
||||
var key = field.key + ':' + subfield.id;
|
||||
|
||||
var value = this.value;
|
||||
if (!onInput) value = context.cleanTagValue(value);
|
||||
var value = this.value;
|
||||
if (!onInput) value = context.cleanTagValue(value);
|
||||
|
||||
// don't override multiple values with blank string
|
||||
if (Array.isArray(_tags[key]) && !value) return;
|
||||
// don't override multiple values with blank string
|
||||
if (Array.isArray(_tags[key]) && !value) return;
|
||||
|
||||
tags[key] = value || undefined;
|
||||
});
|
||||
if (subfield.isAutoStreetPlace) {
|
||||
if (subfield.id === 'street') {
|
||||
tags[`${field.key}:place`] = undefined;
|
||||
} else if (subfield.id === 'place') {
|
||||
tags[`${field.key}:street`] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch.call('change', this, tags, onInput);
|
||||
tags[key] = value || undefined;
|
||||
});
|
||||
|
||||
dispatch.call('change', this, tags, onInput);
|
||||
}, 0);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function updatePlaceholder(inputSelection) {
|
||||
return inputSelection.attr('placeholder', function(subfield) {
|
||||
if (_tags && Array.isArray(_tags[field.key + ':' + subfield.id])) {
|
||||
return t('inspector.multiple_values');
|
||||
}
|
||||
if (_countryCode) {
|
||||
var localkey = subfield.id + '!' + _countryCode;
|
||||
var tkey = addrField.hasTextForStringId('placeholders.' + localkey) ? localkey : subfield.id;
|
||||
return addrField.t('placeholders.' + tkey);
|
||||
if (subfield.isAutoStreetPlace) {
|
||||
return `${getLocalPlaceholder('street')} / ${getLocalPlaceholder('place')}`;
|
||||
}
|
||||
return getLocalPlaceholder(subfield.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getLocalPlaceholder(key) {
|
||||
if (_countryCode) {
|
||||
var localkey = key + '!' + _countryCode;
|
||||
var tkey = addrField.hasTextForStringId('placeholders.' + localkey) ? localkey : key;
|
||||
return addrField.t('placeholders.' + tkey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateTags(tags) {
|
||||
utilGetSetValue(_wrap.selectAll('input'), function (subfield) {
|
||||
var val = tags[field.key + ':' + subfield.id];
|
||||
utilGetSetValue(_wrap.selectAll('input'), subfield => {
|
||||
var val;
|
||||
if (subfield.isAutoStreetPlace) {
|
||||
const streetKey = `${field.key}:street`;
|
||||
const placeKey = `${field.key}:place`;
|
||||
|
||||
if (tags[streetKey] !== undefined || tags[placeKey] === undefined) {
|
||||
val = tags[streetKey];
|
||||
subfield.id = 'street';
|
||||
} else {
|
||||
val = tags[placeKey];
|
||||
subfield.id = 'place';
|
||||
}
|
||||
} else {
|
||||
val = tags[`${field.key}:${subfield.id}`];
|
||||
}
|
||||
return typeof val === 'string' ? val : '';
|
||||
})
|
||||
.attr('title', function(subfield) {
|
||||
|
||||
+86
-34
@@ -1,7 +1,7 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import { drag as d3_drag } from 'd3-drag';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { fileFetcher } from '../../core/file_fetcher';
|
||||
import { osmEntity } from '../../osm/entity';
|
||||
@@ -73,7 +73,7 @@ export function uiFieldCombo(field, context) {
|
||||
function tagValue(dval) {
|
||||
dval = clean(dval || '');
|
||||
|
||||
var found = getOptions().find(function(o) {
|
||||
var found = getOptions(true).find(function(o) {
|
||||
return o.key && clean(o.value) === dval;
|
||||
});
|
||||
if (found) return found.key;
|
||||
@@ -151,7 +151,7 @@ export function uiFieldCombo(field, context) {
|
||||
function objectDifference(a, b) {
|
||||
return a.filter(function(d1) {
|
||||
return !b.some(function(d2) {
|
||||
return !d2.isMixed && d1.value === d2.value;
|
||||
return d1.value === d2.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -167,15 +167,21 @@ export function uiFieldCombo(field, context) {
|
||||
setTaginfoValues('', setPlaceholder);
|
||||
} else {
|
||||
selection.call(_combobox, attachTo);
|
||||
setStaticValues(setPlaceholder);
|
||||
setTimeout(() => setStaticValues(setPlaceholder), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
function getOptions(allOptions) {
|
||||
var stringsField = field.resolveReference('stringsCrossReference');
|
||||
if (!(field.options || stringsField.options)) return [];
|
||||
|
||||
return (field.options || stringsField.options).map(function(v) {
|
||||
let options;
|
||||
if (allOptions !== true) {
|
||||
options = field.options || stringsField.options;
|
||||
} else {
|
||||
options = [].concat(field.options, stringsField.options).filter(Boolean);
|
||||
}
|
||||
return options.map(function(v) {
|
||||
const labelId = getLabelId(stringsField, v);
|
||||
return {
|
||||
key: v,
|
||||
@@ -325,6 +331,12 @@ export function uiFieldCombo(field, context) {
|
||||
|
||||
_container.selectAll('input')
|
||||
.attr('placeholder', ph);
|
||||
|
||||
// Hide 'Add' button if this field uses fixed set of
|
||||
// options and they're all currently used
|
||||
var hideAdd = (!_allowCustomValues && !values.length);
|
||||
_container.selectAll('.chiplist .input-wrap')
|
||||
.style('display', hideAdd ? 'none' : null);
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +416,17 @@ export function uiFieldCombo(field, context) {
|
||||
}
|
||||
|
||||
|
||||
function invertMultikey(d3_event, d) {
|
||||
d3_event.preventDefault();
|
||||
d3_event.stopPropagation();
|
||||
var t = {};
|
||||
if (_isMulti) {
|
||||
t[d.key] = _tags[d.key] === 'yes' ? 'no' : 'yes';
|
||||
}
|
||||
dispatch.call('change', this, t);
|
||||
}
|
||||
|
||||
|
||||
function combo(selection) {
|
||||
_container = selection.selectAll('.form-field-input-wrap')
|
||||
.data([0]);
|
||||
@@ -443,6 +466,11 @@ export function uiFieldCombo(field, context) {
|
||||
.attr('class', 'input-wrap')
|
||||
.merge(_inputWrap);
|
||||
|
||||
// Hide 'Add' button if this field uses fixed set of
|
||||
// options and they're all currently used
|
||||
var hideAdd = (!_allowCustomValues && !_comboData.length);
|
||||
_inputWrap.style('display', hideAdd ? 'none' : null);
|
||||
|
||||
_input = _inputWrap.selectAll('input')
|
||||
.data([0]);
|
||||
} else {
|
||||
@@ -515,11 +543,15 @@ export function uiFieldCombo(field, context) {
|
||||
|
||||
function updateIcon(value) {
|
||||
value = tagValue(value);
|
||||
let container = _container;
|
||||
if (field.type === 'multiCombo' || field.type === 'semiCombo') {
|
||||
container = _container.select('.input-wrap');
|
||||
}
|
||||
const iconsField = field.resolveReference('iconsCrossReference');
|
||||
if (iconsField.icons) {
|
||||
_container.selectAll('.tag-value-icon').remove();
|
||||
container.selectAll('.tag-value-icon').remove();
|
||||
if (iconsField.icons[value]) {
|
||||
_container.selectAll('.tag-value-icon')
|
||||
container.selectAll('.tag-value-icon')
|
||||
.data([value])
|
||||
.enter()
|
||||
.insert('div', 'input')
|
||||
@@ -533,6 +565,14 @@ export function uiFieldCombo(field, context) {
|
||||
_tags = tags;
|
||||
var stringsField = field.resolveReference('stringsCrossReference');
|
||||
|
||||
var isMixed = Array.isArray(tags[field.key]);
|
||||
var showsValue = value => !isMixed && value && !(field.type === 'typeCombo' && value === 'yes');
|
||||
var isRawValue = value => showsValue(value)
|
||||
&& !stringsField.hasTextForStringId(`options.${value}`)
|
||||
&& !stringsField.hasTextForStringId(`options.${value}.title`);
|
||||
var isKnownValue = value => showsValue(value) && !isRawValue(value);
|
||||
var isReadOnly = !_allowCustomValues;
|
||||
|
||||
if (_isMulti || _isSemi) {
|
||||
_multiData = [];
|
||||
|
||||
@@ -545,13 +585,13 @@ export function uiFieldCombo(field, context) {
|
||||
if (!field.key && field.keys.indexOf(k) === -1) continue;
|
||||
|
||||
var v = tags[k];
|
||||
if (!v || (typeof v === 'string' && v.toLowerCase() === 'no')) continue;
|
||||
|
||||
var suffix = field.key ? k.slice(field.key.length) : k;
|
||||
_multiData.push({
|
||||
key: k,
|
||||
value: displayValue(suffix),
|
||||
display: renderValue(suffix),
|
||||
display: addComboboxIcons(renderValue(suffix), suffix),
|
||||
state: typeof v === 'string' ? v.toLowerCase() : '',
|
||||
isMixed: Array.isArray(v)
|
||||
});
|
||||
}
|
||||
@@ -592,7 +632,7 @@ export function uiFieldCombo(field, context) {
|
||||
return {
|
||||
key: v,
|
||||
value: displayValue(v),
|
||||
display: renderValue(v),
|
||||
display: addComboboxIcons(renderValue(v), v),
|
||||
isMixed: !commonValues.includes(v)
|
||||
};
|
||||
});
|
||||
@@ -610,20 +650,13 @@ export function uiFieldCombo(field, context) {
|
||||
// a negative maxlength doesn't make sense
|
||||
maxLength = Math.max(0, maxLength);
|
||||
|
||||
var allowDragAndDrop = _isSemi // only semiCombo values are ordered
|
||||
&& !Array.isArray(tags[field.key]);
|
||||
|
||||
// Exclude existing multikeys from combo options..
|
||||
var available = objectDifference(_comboData, _multiData);
|
||||
_combobox.data(available);
|
||||
|
||||
// Hide 'Add' button if this field uses fixed set of
|
||||
// options and they're all currently used,
|
||||
// or if the field is already at its character limit
|
||||
var hideAdd = (!_allowCustomValues && !available.length) || maxLength <= 0;
|
||||
// Hide 'Add' button if this field is already at its character limit
|
||||
var hideAdd = maxLength <= 0 || (!_allowCustomValues && !_comboData.length);
|
||||
_container.selectAll('.chiplist .input-wrap')
|
||||
.style('display', hideAdd ? 'none' : null);
|
||||
|
||||
var allowDragAndDrop = _isSemi // only semiCombo values are ordered
|
||||
&& !Array.isArray(tags[field.key]);
|
||||
|
||||
// Render chips
|
||||
var chips = _container.selectAll('.chip')
|
||||
@@ -651,8 +684,24 @@ export function uiFieldCombo(field, context) {
|
||||
return d.isMixed;
|
||||
})
|
||||
.attr('title', function(d) {
|
||||
return d.isMixed ? t('inspector.unshared_value_tooltip') : null;
|
||||
});
|
||||
if (d.isMixed) {
|
||||
return t('inspector.unshared_value_tooltip');
|
||||
}
|
||||
if (!['yes', 'no'].includes(d.state)) {
|
||||
return d.state;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.classed('negated', d => d.state === 'no');
|
||||
|
||||
if (!_isSemi) {
|
||||
chips.selectAll('input[type=checkbox]').remove();
|
||||
chips.insert('input', 'span')
|
||||
.attr('type', 'checkbox')
|
||||
.property('checked', d => d.state === 'yes')
|
||||
.property('indeterminate', d => d.isMixed || !['yes', 'no'].includes(d.state))
|
||||
.on('click', invertMultikey);
|
||||
}
|
||||
|
||||
if (allowDragAndDrop) {
|
||||
registerDragAndDrop(chips);
|
||||
@@ -674,21 +723,14 @@ export function uiFieldCombo(field, context) {
|
||||
.attr('class', 'remove')
|
||||
.text('×');
|
||||
|
||||
updateIcon('');
|
||||
} else {
|
||||
var isMixed = Array.isArray(tags[field.key]);
|
||||
|
||||
var mixedValues = isMixed && tags[field.key].map(function(val) {
|
||||
return displayValue(val);
|
||||
}).filter(Boolean);
|
||||
|
||||
var showsValue = !isMixed && tags[field.key] && !(field.type === 'typeCombo' && tags[field.key] === 'yes');
|
||||
var isRawValue = showsValue && !stringsField.hasTextForStringId(`options.${tags[field.key]}`)
|
||||
&& !stringsField.hasTextForStringId(`options.${tags[field.key]}.title`);
|
||||
var isKnownValue = showsValue && !isRawValue;
|
||||
|
||||
var isReadOnly = !_allowCustomValues || isKnownValue;
|
||||
|
||||
utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')
|
||||
.data([tags[field.key]])
|
||||
.classed('raw-value', isRawValue)
|
||||
.classed('known-value', isKnownValue)
|
||||
.attr('readonly', isReadOnly ? 'readonly' : undefined)
|
||||
@@ -697,7 +739,7 @@ export function uiFieldCombo(field, context) {
|
||||
.classed('mixed', isMixed)
|
||||
.on('keydown.deleteCapture', function(d3_event) {
|
||||
if (isReadOnly &&
|
||||
isKnownValue &&
|
||||
isKnownValue(tags[field.key]) &&
|
||||
(d3_event.keyCode === utilKeybinding.keyCodes['⌫'] ||
|
||||
d3_event.keyCode === utilKeybinding.keyCodes['⌦'])) {
|
||||
|
||||
@@ -718,6 +760,16 @@ export function uiFieldCombo(field, context) {
|
||||
_lengthIndicator.update(tags[field.key]);
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStyles = () => {
|
||||
_input
|
||||
.data([tagValue(utilGetSetValue(_input))])
|
||||
.classed('raw-value', isRawValue)
|
||||
.classed('known-value', isKnownValue);
|
||||
};
|
||||
_input.on('input.refreshStyles', refreshStyles);
|
||||
_combobox.on('update.refreshStyles', refreshStyles);
|
||||
refreshStyles();
|
||||
};
|
||||
|
||||
function registerDragAndDrop(selection) {
|
||||
|
||||
+111
-27
@@ -1,7 +1,7 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import _debounce from 'lodash-es/debounce';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { presetManager } from '../../presets';
|
||||
import { fileFetcher } from '../../core/file_fetcher';
|
||||
@@ -11,6 +11,7 @@ import { svgIcon } from '../../svg/icon';
|
||||
import { cardinal } from '../../osm/node';
|
||||
import { uiLengthIndicator } from '..';
|
||||
import { uiTooltip } from '../tooltip';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export {
|
||||
uiFieldText as uiFieldColour,
|
||||
@@ -18,9 +19,11 @@ export {
|
||||
uiFieldText as uiFieldIdentifier,
|
||||
uiFieldText as uiFieldNumber,
|
||||
uiFieldText as uiFieldTel,
|
||||
uiFieldText as uiFieldUrl
|
||||
uiFieldText as uiFieldUrl,
|
||||
likelyRawNumberFormat
|
||||
};
|
||||
|
||||
const likelyRawNumberFormat = /^-?(0\.\d*|\d*\.\d{0,2}(\d{4,})?|\d{4,}\.\d{3})$/;
|
||||
|
||||
export function uiFieldText(field, context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
@@ -32,6 +35,9 @@ export function uiFieldText(field, context) {
|
||||
var _tags;
|
||||
var _phoneFormats = {};
|
||||
const isDirectionField = field.key.split(':').some(keyPart => keyPart === 'direction');
|
||||
const formatFloat = localizer.floatFormatter(localizer.languageCode());
|
||||
const parseLocaleFloat = localizer.floatParser(localizer.languageCode());
|
||||
const countDecimalPlaces = localizer.decimalPlaceCounter(localizer.languageCode());
|
||||
|
||||
if (field.type === 'tel') {
|
||||
fileFetcher.get('phone_formats')
|
||||
@@ -132,18 +138,20 @@ export function uiFieldText(field, context) {
|
||||
var raw_vals = input.node().value || '0';
|
||||
var vals = raw_vals.split(';');
|
||||
vals = vals.map(function(v) {
|
||||
var num = Number(v);
|
||||
v = v.trim();
|
||||
const isRawNumber = likelyRawNumberFormat.test(v);
|
||||
var num = isRawNumber ? parseFloat(v) : parseLocaleFloat(v);
|
||||
if (isDirectionField) {
|
||||
const compassDir = cardinal[v.trim().toLowerCase()];
|
||||
const compassDir = cardinal[v.toLowerCase()];
|
||||
if (compassDir !== undefined) {
|
||||
num = compassDir;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFinite(num)) {
|
||||
// do nothing if the value is neither a number, nor a cardinal direction
|
||||
return v.trim();
|
||||
}
|
||||
// do nothing if the value is neither a number, nor a cardinal direction
|
||||
if (!isFinite(num)) return v;
|
||||
num = parseFloat(num);
|
||||
if (!isFinite(num)) return v;
|
||||
|
||||
num += d;
|
||||
// clamp to 0..359 degree range if it's a direction field
|
||||
@@ -152,8 +160,9 @@ export function uiFieldText(field, context) {
|
||||
num = ((num % 360) + 360) % 360;
|
||||
}
|
||||
// make sure no extra decimals are introduced
|
||||
const numDecimals = v.includes('.') ? v.split('.')[1].length : 0;
|
||||
return clamped(num).toFixed(numDecimals);
|
||||
return formatFloat(clamped(num), isRawNumber
|
||||
? (v.includes('.') ? v.split('.')[1].length : 0)
|
||||
: countDecimalPlaces(v));
|
||||
});
|
||||
input.node().value = vals.join(';');
|
||||
change()();
|
||||
@@ -186,6 +195,7 @@ export function uiFieldText(field, context) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
})
|
||||
.classed('disabled', () => !validIdentifierValueForLink())
|
||||
.merge(outlinkButton);
|
||||
} else if (field.type === 'url') {
|
||||
input.attr('type', 'text');
|
||||
@@ -366,7 +376,7 @@ export function uiFieldText(field, context) {
|
||||
}
|
||||
}
|
||||
if (field.type === 'identifier' && field.pattern) {
|
||||
return value && value.match(new RegExp(field.pattern))[0];
|
||||
return value && value.match(new RegExp(field.pattern))?.[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -384,6 +394,26 @@ export function uiFieldText(field, context) {
|
||||
}
|
||||
|
||||
|
||||
// returns all values of a (potential) multiselection and/or multi-key field
|
||||
function getVals(tags) {
|
||||
if (field.keys) {
|
||||
const multiSelection = context.selectedIDs();
|
||||
tags = multiSelection.length > 1
|
||||
? context.selectedIDs()
|
||||
.map(id => context.graph().entity(id))
|
||||
.map(entity => entity.tags)
|
||||
: [tags];
|
||||
return tags.map(tags => new Set(field.keys
|
||||
.reduce((acc, key) => acc.concat(tags[key]), [])
|
||||
.filter(Boolean)))
|
||||
.map(vals => vals.size === 0 ? new Set([undefined]) : vals)
|
||||
.reduce((a, b) => new Set([...a, ...b]));
|
||||
} else {
|
||||
return new Set([].concat(tags[field.key]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function change(onInput) {
|
||||
return function() {
|
||||
var t = {};
|
||||
@@ -391,21 +421,42 @@ export function uiFieldText(field, context) {
|
||||
if (!onInput) val = context.cleanTagValue(val);
|
||||
|
||||
// don't override multiple values with blank string
|
||||
if (!val && Array.isArray(_tags[field.key])) return;
|
||||
if (!val && getVals(_tags).size > 1) return;
|
||||
|
||||
if (!onInput) {
|
||||
if (field.type === 'number' && val) {
|
||||
var vals = val.split(';');
|
||||
vals = vals.map(function(v) {
|
||||
var num = Number(v);
|
||||
return isFinite(num) ? clamped(num) : v.trim();
|
||||
});
|
||||
val = vals.join(';');
|
||||
}
|
||||
utilGetSetValue(input, val);
|
||||
var displayVal = val;
|
||||
if (field.type === 'number' && val) {
|
||||
var numbers = val.split(';');
|
||||
numbers = numbers.map(function(v) {
|
||||
if (likelyRawNumberFormat.test(v)) {
|
||||
// input number likely in "raw" format
|
||||
return v;
|
||||
}
|
||||
var num = parseLocaleFloat(v);
|
||||
const fractionDigits = countDecimalPlaces(v);
|
||||
return isFinite(num) ? clamped(num).toFixed(fractionDigits) : v;
|
||||
});
|
||||
val = numbers.join(';');
|
||||
}
|
||||
if (!onInput) utilGetSetValue(input, displayVal);
|
||||
t[field.key] = val || undefined;
|
||||
dispatch.call('change', this, t, onInput);
|
||||
if (field.keys) {
|
||||
// for multi-key fields with: handle alternative tag keys gracefully
|
||||
// https://github.com/openstreetmap/id-tagging-schema/issues/905
|
||||
dispatch.call('change', this, tags => {
|
||||
if (field.keys.some(key => tags[key])) {
|
||||
// use exiting key(s)
|
||||
field.keys.filter(key => tags[key]).forEach(key => {
|
||||
tags[key] = val || undefined;
|
||||
});
|
||||
} else {
|
||||
// fall back to default key if none of the `keys` is preset
|
||||
tags[field.key] = val || undefined;
|
||||
}
|
||||
return tags;
|
||||
}, onInput);
|
||||
} else {
|
||||
dispatch.call('change', this, t, onInput);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -416,14 +467,47 @@ export function uiFieldText(field, context) {
|
||||
return i;
|
||||
};
|
||||
|
||||
|
||||
i.tags = function(tags) {
|
||||
_tags = tags;
|
||||
|
||||
var isMixed = Array.isArray(tags[field.key]);
|
||||
const vals = getVals(tags);
|
||||
const isMixed = vals.size > 1;
|
||||
var val = vals.size === 1 ? [...vals][0] : '';
|
||||
var shouldUpdate;
|
||||
|
||||
utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '')
|
||||
.attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined)
|
||||
if (field.type === 'number' && val) {
|
||||
var numbers = val.split(';');
|
||||
var oriNumbers = utilGetSetValue(input).split(';');
|
||||
if (numbers.length !== oriNumbers.length) shouldUpdate = true;
|
||||
numbers = numbers.map(function(v) {
|
||||
v = v.trim();
|
||||
var num = Number(v);
|
||||
if (!isFinite(num) || v === '') return v;
|
||||
const fractionDigits = v.includes('.') ? v.split('.')[1].length : 0;
|
||||
return formatFloat(num, fractionDigits);
|
||||
});
|
||||
val = numbers.join(';');
|
||||
// for number fields, we don't want to override the content of the
|
||||
// input element with the same number using a different formatting
|
||||
// (e.g. when entering "1234.5", this should not be reformatted to
|
||||
// "1.234,5" which could otherwise cause the cursor to be in the
|
||||
// wrong location after the change)
|
||||
// but if the actual numeric value of the field has changed (e.g.
|
||||
// by pressing the +/- buttons or using the raw tag editor), we
|
||||
// can and should update the content of the input element.
|
||||
shouldUpdate = (inputValue, setValue) => {
|
||||
const inputNums = inputValue.split(';').map(setVal =>
|
||||
likelyRawNumberFormat.test(setVal)
|
||||
? parseFloat(setVal)
|
||||
: parseLocaleFloat(setVal)
|
||||
);
|
||||
const setNums = setValue.split(';').map(parseLocaleFloat);
|
||||
return !isEqual(inputNums, setNums);
|
||||
};
|
||||
}
|
||||
|
||||
utilGetSetValue(input, val, shouldUpdate)
|
||||
.attr('title', isMixed ? [...vals].join('\n') : undefined)
|
||||
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown')))
|
||||
.classed('mixed', isMixed);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { presetManager } from '../../presets';
|
||||
import { fileFetcher } from '../../core/file_fetcher';
|
||||
@@ -96,7 +96,7 @@ export function uiFieldLocalized(field, context) {
|
||||
var preset = presetManager.match(entity, context.graph());
|
||||
if (preset) {
|
||||
var isSuggestion = preset.suggestion;
|
||||
var fields = preset.fields();
|
||||
var fields = preset.fields(entity.extent(context.graph()).center());
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { uiCombobox } from '../combobox';
|
||||
import { t } from '../../core/localizer';
|
||||
import { t, localizer } from '../../core/localizer';
|
||||
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';
|
||||
import { likelyRawNumberFormat } from './input';
|
||||
|
||||
|
||||
export function uiFieldRoadheight(field, context) {
|
||||
@@ -16,6 +17,8 @@ export function uiFieldRoadheight(field, context) {
|
||||
var _entityIDs = [];
|
||||
var _tags;
|
||||
var _isImperial;
|
||||
var formatFloat = localizer.floatFormatter(localizer.languageCode());
|
||||
var parseLocaleFloat = localizer.floatParser(localizer.languageCode());
|
||||
|
||||
var primaryUnits = [
|
||||
{
|
||||
@@ -129,16 +132,27 @@ export function uiFieldRoadheight(field, context) {
|
||||
|
||||
if (!primaryValue && !secondaryValue) {
|
||||
tag[field.key] = undefined;
|
||||
} else if (isNaN(primaryValue) || isNaN(secondaryValue) || !_isImperial) {
|
||||
tag[field.key] = context.cleanTagValue(primaryValue);
|
||||
} else {
|
||||
if (primaryValue !== '') {
|
||||
primaryValue = context.cleanTagValue(primaryValue + '\'');
|
||||
var rawPrimaryValue = likelyRawNumberFormat.test(primaryValue)
|
||||
? parseFloat(primaryValue)
|
||||
: parseLocaleFloat(primaryValue);
|
||||
if (isNaN(rawPrimaryValue)) rawPrimaryValue = primaryValue;
|
||||
var rawSecondaryValue = likelyRawNumberFormat.test(secondaryValue)
|
||||
? parseFloat(secondaryValue)
|
||||
: parseLocaleFloat(secondaryValue);
|
||||
if (isNaN(rawSecondaryValue)) rawSecondaryValue = secondaryValue;
|
||||
|
||||
if (isNaN(rawPrimaryValue) || isNaN(rawSecondaryValue) || !_isImperial) {
|
||||
tag[field.key] = context.cleanTagValue(rawPrimaryValue);
|
||||
} else {
|
||||
if (rawPrimaryValue !== '') {
|
||||
rawPrimaryValue = rawPrimaryValue + '\'';
|
||||
}
|
||||
if (rawSecondaryValue !== '') {
|
||||
rawSecondaryValue = rawSecondaryValue + '"';
|
||||
}
|
||||
tag[field.key] = context.cleanTagValue(rawPrimaryValue + rawSecondaryValue);
|
||||
}
|
||||
if (secondaryValue !== '') {
|
||||
secondaryValue = context.cleanTagValue(secondaryValue + '"');
|
||||
}
|
||||
tag[field.key] = primaryValue + secondaryValue;
|
||||
}
|
||||
|
||||
dispatch.call('change', this, tag);
|
||||
@@ -156,26 +170,36 @@ export function uiFieldRoadheight(field, context) {
|
||||
if (primaryValue && (primaryValue.indexOf('\'') >= 0 || primaryValue.indexOf('"') >= 0)) {
|
||||
secondaryValue = primaryValue.match(/(-?[\d.]+)"/);
|
||||
if (secondaryValue !== null) {
|
||||
secondaryValue = secondaryValue[1];
|
||||
secondaryValue = formatFloat(parseFloat(secondaryValue[1]));
|
||||
}
|
||||
primaryValue = primaryValue.match(/(-?[\d.]+)'/);
|
||||
if (primaryValue !== null) {
|
||||
primaryValue = primaryValue[1];
|
||||
primaryValue = formatFloat(parseFloat(primaryValue[1]));
|
||||
}
|
||||
_isImperial = true;
|
||||
} else if (primaryValue) {
|
||||
var rawValue = primaryValue;
|
||||
primaryValue = parseFloat(rawValue);
|
||||
if (isNaN(primaryValue)) {
|
||||
primaryValue = rawValue;
|
||||
} else {
|
||||
primaryValue = formatFloat(primaryValue);
|
||||
}
|
||||
_isImperial = false;
|
||||
}
|
||||
}
|
||||
|
||||
setUnitSuggestions();
|
||||
|
||||
// If feet are specified but inches are omitted, assume zero inches.
|
||||
var inchesPlaceholder = formatFloat(0);
|
||||
|
||||
utilGetSetValue(primaryInput, typeof primaryValue === 'string' ? primaryValue : '')
|
||||
.attr('title', isMixed ? primaryValue.filter(Boolean).join('\n') : null)
|
||||
.attr('placeholder', isMixed ? t('inspector.multiple_values') : t('inspector.unknown'))
|
||||
.classed('mixed', isMixed);
|
||||
utilGetSetValue(secondaryInput, typeof secondaryValue === 'string' ? secondaryValue : '')
|
||||
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (_isImperial ? '0' : null))
|
||||
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (_isImperial ? inchesPlaceholder : null))
|
||||
.classed('mixed', isMixed)
|
||||
.classed('disabled', !_isImperial)
|
||||
.attr('readonly', _isImperial ? null : 'readonly');
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import * as countryCoder from '@ideditor/country-coder';
|
||||
import * as countryCoder from '@rapideditor/country-coder';
|
||||
|
||||
import { uiCombobox } from '../combobox';
|
||||
import { t } from '../../core/localizer';
|
||||
import { t, localizer } from '../../core/localizer';
|
||||
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';
|
||||
import { likelyRawNumberFormat } from './input';
|
||||
|
||||
|
||||
export function uiFieldRoadspeed(field, context) {
|
||||
@@ -14,6 +15,8 @@ export function uiFieldRoadspeed(field, context) {
|
||||
var _entityIDs = [];
|
||||
var _tags;
|
||||
var _isImperial;
|
||||
var formatFloat = localizer.floatFormatter(localizer.languageCode());
|
||||
var parseLocaleFloat = localizer.floatParser(localizer.languageCode());
|
||||
|
||||
var speedCombo = uiCombobox(context, 'roadspeed');
|
||||
var unitCombo = uiCombobox(context, 'roadspeed-unit')
|
||||
@@ -91,8 +94,8 @@ export function uiFieldRoadspeed(field, context) {
|
||||
|
||||
function comboValues(d) {
|
||||
return {
|
||||
value: d.toString(),
|
||||
title: d.toString()
|
||||
value: formatFloat(d),
|
||||
title: formatFloat(d)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,10 +109,16 @@ export function uiFieldRoadspeed(field, context) {
|
||||
|
||||
if (!value) {
|
||||
tag[field.key] = undefined;
|
||||
} else if (isNaN(value) || !_isImperial) {
|
||||
tag[field.key] = context.cleanTagValue(value);
|
||||
} else {
|
||||
tag[field.key] = context.cleanTagValue(value + ' mph');
|
||||
var rawValue = likelyRawNumberFormat.test(value)
|
||||
? parseFloat(value)
|
||||
: parseLocaleFloat(value);
|
||||
if (isNaN(rawValue)) rawValue = value;
|
||||
if (isNaN(rawValue) || !_isImperial) {
|
||||
tag[field.key] = context.cleanTagValue(rawValue);
|
||||
} else {
|
||||
tag[field.key] = context.cleanTagValue(rawValue + ' mph');
|
||||
}
|
||||
}
|
||||
|
||||
dispatch.call('change', this, tag);
|
||||
@@ -119,16 +128,23 @@ export function uiFieldRoadspeed(field, context) {
|
||||
roadspeed.tags = function(tags) {
|
||||
_tags = tags;
|
||||
|
||||
var value = tags[field.key];
|
||||
var rawValue = tags[field.key];
|
||||
var value = rawValue;
|
||||
var isMixed = Array.isArray(value);
|
||||
|
||||
if (!isMixed) {
|
||||
if (value && value.indexOf('mph') >= 0) {
|
||||
value = parseInt(value, 10).toString();
|
||||
if (rawValue && rawValue.indexOf('mph') >= 0) {
|
||||
_isImperial = true;
|
||||
} else if (value) {
|
||||
} else if (rawValue) {
|
||||
_isImperial = false;
|
||||
}
|
||||
|
||||
value = parseInt(value, 10);
|
||||
if (isNaN(value)) {
|
||||
value = rawValue;
|
||||
} else {
|
||||
value = formatFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
setUnitSuggestions();
|
||||
|
||||
@@ -146,7 +146,10 @@ export function uiFieldWikidata(field, context) {
|
||||
}
|
||||
|
||||
wikidata.itemsForSearchQuery(q, function(err, data) {
|
||||
if (err) return;
|
||||
if (err) {
|
||||
if (err !== 'No query') console.error(err); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
var result = data.map(function (item) {
|
||||
return {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function uiPhotoviewer(context) {
|
||||
target.style('height', newHeight + 'px');
|
||||
}
|
||||
|
||||
dispatch.call(eventName, target, utilGetDimensions(target, true));
|
||||
dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target));
|
||||
}
|
||||
|
||||
function clamp(num, min, max) {
|
||||
@@ -151,9 +151,16 @@ export function uiPhotoviewer(context) {
|
||||
.style('width', setPhotoDimensions[0] + 'px')
|
||||
.style('height', setPhotoDimensions[1] + 'px');
|
||||
|
||||
dispatch.call('resize', photoviewer, setPhotoDimensions);
|
||||
dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));
|
||||
}
|
||||
};
|
||||
|
||||
function subtractPadding(dimensions, selection) {
|
||||
return [
|
||||
dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),
|
||||
dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))
|
||||
];
|
||||
}
|
||||
|
||||
return utilRebind(photoviewer, dispatch, 'on');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { presetManager } from '../../presets';
|
||||
import { t, localizer } from '../../core/localizer';
|
||||
import { utilArrayIdentical } from '../../util/array';
|
||||
import { utilArrayUnion, utilRebind } from '../../util';
|
||||
import { geoExtent } from '../../geo/extent';
|
||||
import { uiField } from '../field';
|
||||
import { uiFormFields } from '../form_fields';
|
||||
import { uiSection } from '../section';
|
||||
@@ -32,6 +33,11 @@ export function uiSectionPresetFields(context) {
|
||||
return geoms;
|
||||
}, {}));
|
||||
|
||||
const loc = _entityIDs.reduce(function(extent, entityID) {
|
||||
var entity = context.graph().entity(entityID);
|
||||
return extent.extend(entity.extent(context.graph()));
|
||||
}, geoExtent()).center();
|
||||
|
||||
var presetsManager = presetManager;
|
||||
|
||||
var allFields = [];
|
||||
@@ -39,8 +45,8 @@ export function uiSectionPresetFields(context) {
|
||||
var sharedTotalFields;
|
||||
|
||||
_presets.forEach(function(preset) {
|
||||
var fields = preset.fields();
|
||||
var moreFields = preset.moreFields();
|
||||
var fields = preset.fields(loc);
|
||||
var moreFields = preset.moreFields(loc);
|
||||
|
||||
allFields = utilArrayUnion(allFields, fields);
|
||||
allMoreFields = utilArrayUnion(allMoreFields, moreFields);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { localizer, t } from '../../core/localizer';
|
||||
import { utilArrayDifference, utilArrayIdentical } from '../../util/array';
|
||||
import { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';
|
||||
import { uiTooltip } from '..';
|
||||
import { allowUpperCaseTagValues } from '../../osm/tags';
|
||||
|
||||
|
||||
export function uiSectionRawTagEditor(id, context) {
|
||||
@@ -421,7 +422,9 @@ export function uiSectionRawTagEditor(id, context) {
|
||||
query: value
|
||||
}, function(err, data) {
|
||||
if (!err) {
|
||||
var filtered = data.filter(function(d) { return _tags[d.value] === undefined; });
|
||||
const filtered = data
|
||||
.filter(d => _tags[d.value] === undefined)
|
||||
.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));
|
||||
callback(sort(value, filtered));
|
||||
}
|
||||
});
|
||||
@@ -435,9 +438,13 @@ export function uiSectionRawTagEditor(id, context) {
|
||||
geometry: geometry,
|
||||
query: value
|
||||
}, function(err, data) {
|
||||
if (!err) callback(sort(value, data));
|
||||
if (!err) {
|
||||
const filtered = data.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));
|
||||
callback(sort(value, filtered));
|
||||
}
|
||||
});
|
||||
}));
|
||||
})
|
||||
.caseSensitive(allowUpperCaseTagValues.test(utilGetSetValue(key))));
|
||||
|
||||
|
||||
function sort(value, data) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Like selection.property('value', ...), but avoids no-op value sets,
|
||||
// which can result in layout/repaint thrashing in some situations.
|
||||
/** @returns {string} */
|
||||
export function utilGetSetValue(selection, value) {
|
||||
function d3_selection_value(value) {
|
||||
export function utilGetSetValue(selection, value, shouldUpdate) {
|
||||
function setValue(value, shouldUpdate) {
|
||||
function valueNull() {
|
||||
delete this.value;
|
||||
}
|
||||
|
||||
function valueConstant() {
|
||||
if (this.value !== value) {
|
||||
if (shouldUpdate(this.value, value)) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function utilGetSetValue(selection, value) {
|
||||
var x = value.apply(this, arguments);
|
||||
if (x === null || x === undefined) {
|
||||
delete this.value;
|
||||
} else if (this.value !== x) {
|
||||
} else if (shouldUpdate(this.value, x)) {
|
||||
this.value = x;
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,28 @@ export function utilGetSetValue(selection, value) {
|
||||
? valueFunction : valueConstant);
|
||||
}
|
||||
|
||||
function stickyCursor(func) {
|
||||
return function() {
|
||||
const cursor = { start: this.selectionStart, end: this.selectionEnd };
|
||||
func.apply(this, arguments);
|
||||
this.setSelectionRange(cursor.start, cursor.end);
|
||||
};
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
return selection.property('value');
|
||||
}
|
||||
|
||||
return selection.each(d3_selection_value(value));
|
||||
if (shouldUpdate === undefined) {
|
||||
shouldUpdate = (a, b) => a !== b;
|
||||
}
|
||||
|
||||
// only certain input element types allow manipulating the cursor
|
||||
// see https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
|
||||
const supportedTypes = ['text', 'search', 'url', 'tel', 'password'];
|
||||
if (!supportedTypes.includes(this.type)) {
|
||||
return selection.each(setValue(value, shouldUpdate));
|
||||
}
|
||||
|
||||
return selection.each(stickyCursor(setValue(value, shouldUpdate)));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { actionAddMidpoint } from '../actions/add_midpoint';
|
||||
import { actionChangeTags } from '../actions/change_tags';
|
||||
import { actionMergeNodes } from '../actions/merge_nodes';
|
||||
@@ -123,9 +125,8 @@ export function validationCrossingWays(context) {
|
||||
motorway: true, motorway_link: true, trunk: true, trunk_link: true,
|
||||
primary: true, primary_link: true, secondary: true, secondary_link: true
|
||||
};
|
||||
var nonCrossingHighways = { track: true };
|
||||
|
||||
function tagsForConnectionNodeIfAllowed(entity1, entity2, graph) {
|
||||
function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {
|
||||
var featureType1 = getFeatureType(entity1, graph);
|
||||
var featureType2 = getFeatureType(entity2, graph);
|
||||
|
||||
@@ -141,11 +142,18 @@ export function validationCrossingWays(context) {
|
||||
// one feature is a path but not both
|
||||
|
||||
var roadFeature = entity1IsPath ? entity2 : entity1;
|
||||
if (nonCrossingHighways[roadFeature.tags.highway]) {
|
||||
// don't mark path connections with certain roads as crossings
|
||||
var pathFeature = entity1IsPath ? entity1 : entity2;
|
||||
// don't mark path connections with tracks as crossings
|
||||
if (roadFeature.tags.highway === 'track') {
|
||||
return {};
|
||||
}
|
||||
// a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway
|
||||
// a sidewalk crossing another kind of service road may be similarly unremarkable
|
||||
if (!lessLikelyTags &&
|
||||
roadFeature.tags.highway === 'service' &&
|
||||
pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {
|
||||
return {};
|
||||
}
|
||||
var pathFeature = entity1IsPath ? entity1 : entity2;
|
||||
if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {
|
||||
// if the path is a crossing, match the crossing type
|
||||
return bothLines ? { highway: 'crossing', crossing: pathFeature.tags.crossing } : {};
|
||||
@@ -435,6 +443,10 @@ export function validationCrossingWays(context) {
|
||||
|
||||
if (connectionTags) {
|
||||
fixes.push(makeConnectWaysFix(this.data.connectionTags));
|
||||
let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);
|
||||
if (lessLikelyConnectionTags && !isEqual(connectionTags, lessLikelyConnectionTags)) {
|
||||
fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));
|
||||
}
|
||||
}
|
||||
|
||||
if (isCrossingIndoors) {
|
||||
@@ -692,16 +704,21 @@ export function validationCrossingWays(context) {
|
||||
function makeConnectWaysFix(connectionTags) {
|
||||
|
||||
var fixTitleID = 'connect_features';
|
||||
var fixIcon = 'iD-icon-crossing';
|
||||
if (connectionTags.highway === 'crossing') {
|
||||
fixTitleID = 'connect_using_crossing';
|
||||
fixIcon = 'temaki-pedestrian';
|
||||
}
|
||||
if (connectionTags.ford) {
|
||||
fixTitleID = 'connect_using_ford';
|
||||
fixIcon = 'roentgen-ford';
|
||||
}
|
||||
|
||||
return new validationIssueFix({
|
||||
icon: 'iD-icon-crossing',
|
||||
const fix = new validationIssueFix({
|
||||
icon: fixIcon,
|
||||
title: t.append('issues.fix.' + fixTitleID + '.title'),
|
||||
onClick: function(context) {
|
||||
var loc = this.issue.loc;
|
||||
var connectionTags = this.issue.data.connectionTags;
|
||||
var edges = this.issue.data.edges;
|
||||
|
||||
context.perform(
|
||||
@@ -737,6 +754,8 @@ export function validationCrossingWays(context) {
|
||||
);
|
||||
}
|
||||
});
|
||||
fix._connectionTags = connectionTags;
|
||||
return fix;
|
||||
}
|
||||
|
||||
function makeChangeLayerFix(higherOrLower) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { actionAddVertex } from '../actions/add_vertex';
|
||||
import { actionChangeTags } from '../actions/change_tags';
|
||||
import { actionMergeNodes } from '../actions/merge_nodes';
|
||||
@@ -27,7 +28,7 @@ export function validationMismatchedGeometry() {
|
||||
|
||||
var asLine = presetManager.matchTags(tagSuggestingArea, 'line');
|
||||
var asArea = presetManager.matchTags(tagSuggestingArea, 'area');
|
||||
if (asLine && asArea && asLine === asArea) {
|
||||
if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {
|
||||
// this tag also allows lines and making this an area wouldn't matter
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user