mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-25 17:37:49 +02:00
Merge branch 'develop' into relation-colours
This commit is contained in:
@@ -44,7 +44,7 @@ export function uiAccount(context) {
|
||||
// Add user name
|
||||
userLink.append('span')
|
||||
.attr('class', 'label')
|
||||
.html(user.display_name);
|
||||
.text(user.display_name);
|
||||
|
||||
// show "Log Out"
|
||||
loginLogout
|
||||
|
||||
@@ -33,7 +33,7 @@ export function uiChangesetEditor(context) {
|
||||
|
||||
_fieldsArr = [
|
||||
uiField(context, presets.field('comment'), null, { show: true, revert: false }),
|
||||
uiField(context, presets.field('source'), null, { show: false, revert: false }),
|
||||
uiField(context, presets.field('source'), null, { show: true, revert: false }),
|
||||
uiField(context, presets.field('hashtags'), null, { show: false, revert: false }),
|
||||
];
|
||||
|
||||
|
||||
@@ -230,12 +230,12 @@ export function uiCombobox(context, klass) {
|
||||
// Called whenever the input value is changed (e.g. on typing)
|
||||
function change(doAutoComplete) {
|
||||
if (doAutoComplete === undefined) doAutoComplete = true;
|
||||
fetchComboData(value(), function() {
|
||||
fetchComboData(value(), function(skipAutosuggest) {
|
||||
_selected = null;
|
||||
var val = input.property('value');
|
||||
|
||||
if (_suggestions.length) {
|
||||
if (doAutoComplete && input.property('selectionEnd') === val.length) {
|
||||
if (doAutoComplete && !skipAutosuggest && input.property('selectionEnd') === val.length) {
|
||||
_selected = tryAutocomplete();
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ export function uiCombobox(context, klass) {
|
||||
function fetchComboData(v, cb) {
|
||||
_cancelFetch = false;
|
||||
|
||||
_fetcher.call(input, v, function(results) {
|
||||
_fetcher.call(input, v, function(results, skipAutosuggest) {
|
||||
// already chose a value, don't overwrite or autocomplete it
|
||||
if (_cancelFetch) return;
|
||||
|
||||
@@ -327,7 +327,7 @@ export function uiCombobox(context, klass) {
|
||||
results.forEach(function(d) { _fetched[d.value] = d; });
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
cb(skipAutosuggest);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ export function uiEditMenu(context) {
|
||||
if (showLabels) {
|
||||
buttonsEnter.append('span')
|
||||
.attr('class', 'label')
|
||||
.html(function(d) {
|
||||
return d.title;
|
||||
.each(function(d) {
|
||||
d3_select(this).call(d.title);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+20
-8
@@ -41,9 +41,6 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
.title(() => t.append('inspector.lock.suggestion', { label: field.title }))
|
||||
.placement('bottom');
|
||||
|
||||
|
||||
field.keys = field.keys || [field.key];
|
||||
|
||||
// only create the fields that are actually being shown
|
||||
if (_show && !field.impl) {
|
||||
createField();
|
||||
@@ -67,12 +64,23 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
}
|
||||
|
||||
|
||||
function allKeys() {
|
||||
let keys = field.keys || [field.key];
|
||||
if (field.type === 'directionalCombo' && field.key) {
|
||||
// directionalCombo fields can have an additional key describing the for
|
||||
// cases where both directions share a "common" value.
|
||||
keys = keys.concat(field.key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
function isModified() {
|
||||
if (!entityIDs || !entityIDs.length) return false;
|
||||
return entityIDs.some(function(entityID) {
|
||||
var original = context.graph().base().entities[entityID];
|
||||
var latest = context.graph().entity(entityID);
|
||||
return field.keys.some(function(key) {
|
||||
return allKeys().some(function(key) {
|
||||
return original ? latest.tags[key] !== original.tags[key] : latest.tags[key];
|
||||
});
|
||||
});
|
||||
@@ -80,7 +88,7 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
|
||||
|
||||
function tagsContainFieldKey() {
|
||||
return field.keys.some(function(key) {
|
||||
return allKeys().some(function(key) {
|
||||
if (field.type === 'multiCombo') {
|
||||
for (var tagKey in _tags) {
|
||||
if (tagKey.indexOf(key) === 0) {
|
||||
@@ -99,7 +107,7 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
d3_event.preventDefault();
|
||||
if (!entityIDs || _locked) return;
|
||||
|
||||
dispatch.call('revert', d, d.keys);
|
||||
dispatch.call('revert', d, allKeys());
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +117,7 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
if (_locked) return;
|
||||
|
||||
var t = {};
|
||||
d.keys.forEach(function(key) {
|
||||
allKeys().forEach(function(key) {
|
||||
t[key] = undefined;
|
||||
});
|
||||
|
||||
@@ -196,7 +204,11 @@ export function uiField(context, presetField, entityIDs, options) {
|
||||
referenceKey = referenceKey.replace(/:$/, '');
|
||||
}
|
||||
|
||||
reference = uiTagReference(d.reference || { key: referenceKey }, context);
|
||||
var referenceOptions = d.reference || {
|
||||
key: referenceKey,
|
||||
value: _tags[referenceKey]
|
||||
};
|
||||
reference = uiTagReference(referenceOptions, context);
|
||||
if (_state === 'hover') {
|
||||
reference.showing(false);
|
||||
}
|
||||
|
||||
@@ -212,6 +212,11 @@ export function uiFieldAccess(field, context) {
|
||||
},
|
||||
construction: {
|
||||
access: 'no'
|
||||
},
|
||||
busway: {
|
||||
access: 'no',
|
||||
bus: 'designated',
|
||||
emergency: 'yes',
|
||||
}
|
||||
},
|
||||
barrier: {
|
||||
|
||||
+150
-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,78 @@ 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;
|
||||
});
|
||||
|
||||
Object.keys(tags)
|
||||
.filter(k => tags[k])
|
||||
.forEach(k => _tags[k] = tags[k]);
|
||||
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) {
|
||||
|
||||
+138
-70
@@ -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';
|
||||
@@ -22,23 +22,6 @@ export {
|
||||
uiFieldCombo as uiFieldTypeCombo
|
||||
};
|
||||
|
||||
const valueIcons = {
|
||||
'crossing:markings': [
|
||||
'dashes',
|
||||
'dots',
|
||||
'ladder:paired',
|
||||
'ladder:skewed',
|
||||
'ladder',
|
||||
'lines:paired',
|
||||
'lines',
|
||||
'surface',
|
||||
'zebra:bicolour',
|
||||
'zebra:double',
|
||||
'zebra:paired',
|
||||
'zebra',
|
||||
]
|
||||
};
|
||||
|
||||
export function uiFieldCombo(field, context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');
|
||||
@@ -90,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;
|
||||
@@ -149,7 +132,7 @@ export function uiFieldCombo(field, context) {
|
||||
var stringsField = field.resolveReference('stringsCrossReference');
|
||||
const labelId = getLabelId(stringsField, tval);
|
||||
if (stringsField.hasTextForStringId(labelId)) {
|
||||
return stringsField.t(labelId, { default: tval });
|
||||
return stringsField.t.append(labelId, { default: tval });
|
||||
}
|
||||
|
||||
if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {
|
||||
@@ -168,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;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -184,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,
|
||||
@@ -205,6 +194,11 @@ export function uiFieldCombo(field, context) {
|
||||
}
|
||||
|
||||
|
||||
function hasStaticValues() {
|
||||
return getOptions().length > 0;
|
||||
}
|
||||
|
||||
|
||||
function setStaticValues(callback, filter) {
|
||||
_comboData = getOptions();
|
||||
|
||||
@@ -220,7 +214,9 @@ export function uiFieldCombo(field, context) {
|
||||
|
||||
function setTaginfoValues(q, callback) {
|
||||
var queryFilter = d => d.value.toLowerCase().includes(q.toLowerCase()) || d.key.toLowerCase().includes(q.toLowerCase());
|
||||
setStaticValues(callback, queryFilter);
|
||||
if (hasStaticValues()) {
|
||||
setStaticValues(callback, queryFilter);
|
||||
}
|
||||
|
||||
var stringsField = field.resolveReference('stringsCrossReference');
|
||||
var fn = _isMulti ? 'multikeys' : 'values';
|
||||
@@ -294,19 +290,20 @@ export function uiFieldCombo(field, context) {
|
||||
_comboData = _comboData.filter(queryFilter);
|
||||
|
||||
_comboData = objectDifference(_comboData, _multiData);
|
||||
if (callback) callback(_comboData);
|
||||
if (callback) callback(_comboData, hasStaticValues());
|
||||
});
|
||||
}
|
||||
|
||||
// adds icons to tag values which have one
|
||||
function addComboboxIcons(disp, value) {
|
||||
if (valueIcons[field.key]) {
|
||||
const iconsField = field.resolveReference('iconsCrossReference');
|
||||
if (iconsField.icons) {
|
||||
return function(selection) {
|
||||
var span = selection
|
||||
.insert('span', ':first-child')
|
||||
.attr('class', 'tag-value-icon');
|
||||
if (valueIcons[field.key].indexOf(value) !== -1) {
|
||||
span.call(svgIcon('#iD-' + field.key.replace(/:/g, '_') + '-' + value.replace(/:/g, '_')));
|
||||
if (iconsField.icons[value]) {
|
||||
span.call(svgIcon(`#${iconsField.icons[value]}`));
|
||||
}
|
||||
disp.call(this, selection);
|
||||
};
|
||||
@@ -341,6 +338,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +356,8 @@ export function uiFieldCombo(field, context) {
|
||||
if (_isMulti) {
|
||||
vals = [tagValue(utilGetSetValue(_input))];
|
||||
} else if (_isSemi) {
|
||||
val = tagValue(utilGetSetValue(_input).replace(/,/g, ';')) || '';
|
||||
val = tagValue(utilGetSetValue(_input)) || '';
|
||||
val = val.replace(/,/g, ';');
|
||||
vals = val.split(';');
|
||||
}
|
||||
vals = vals.filter(Boolean);
|
||||
@@ -419,6 +423,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]);
|
||||
@@ -458,6 +473,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 {
|
||||
@@ -470,7 +490,7 @@ export function uiFieldCombo(field, context) {
|
||||
.attr('type', 'text')
|
||||
.attr('id', field.domId)
|
||||
.call(utilNoAuto)
|
||||
.call(initCombo, selection)
|
||||
.call(initCombo, _container)
|
||||
.merge(_input);
|
||||
|
||||
if (_isSemi) {
|
||||
@@ -530,15 +550,20 @@ export function uiFieldCombo(field, context) {
|
||||
|
||||
function updateIcon(value) {
|
||||
value = tagValue(value);
|
||||
if (valueIcons[field.key]) {
|
||||
_container.selectAll('.tag-value-icon').remove();
|
||||
if (valueIcons[field.key].indexOf(value) !== -1) {
|
||||
_container.selectAll('.tag-value-icon')
|
||||
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();
|
||||
if (iconsField.icons[value]) {
|
||||
container.selectAll('.tag-value-icon')
|
||||
.data([value])
|
||||
.enter()
|
||||
.insert('div', 'input')
|
||||
.attr('class', 'tag-value-icon')
|
||||
.call(svgIcon('#iD-' + field.key.replace(/:/g, '_') + '-' + value.replace(/:/g, '_')));
|
||||
.call(svgIcon(`#${iconsField.icons[value]}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,6 +572,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 = [];
|
||||
|
||||
@@ -559,13 +592,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)
|
||||
});
|
||||
}
|
||||
@@ -606,7 +639,7 @@ export function uiFieldCombo(field, context) {
|
||||
return {
|
||||
key: v,
|
||||
value: displayValue(v),
|
||||
display: renderValue(v),
|
||||
display: addComboboxIcons(renderValue(v), v),
|
||||
isMixed: !commonValues.includes(v)
|
||||
};
|
||||
});
|
||||
@@ -624,20 +657,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,7 +677,12 @@ export function uiFieldCombo(field, context) {
|
||||
.attr('class', 'chip');
|
||||
|
||||
enter.append('span');
|
||||
enter.append('a');
|
||||
const field_buttons = enter
|
||||
.append('div')
|
||||
.attr('class', 'field_buttons');
|
||||
field_buttons
|
||||
.append('a')
|
||||
.attr('class', 'remove');
|
||||
|
||||
chips = chips.merge(enter)
|
||||
.order()
|
||||
@@ -665,44 +696,71 @@ 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);
|
||||
}
|
||||
|
||||
chips.select('span').each(function(d) {
|
||||
chips.each(function(d) {
|
||||
const selection = d3_select(this);
|
||||
if (d.display) {
|
||||
selection.text('');
|
||||
d.display(selection);
|
||||
} else {
|
||||
selection.text(d.value);
|
||||
const text_span = selection.select('span');
|
||||
const field_buttons = selection.select('.field_buttons');
|
||||
const clean_value = d.value.trim();
|
||||
text_span.text('');
|
||||
if (clean_value.startsWith('https://')) {
|
||||
// create a button to open the link in a new tab
|
||||
text_span.text(clean_value);
|
||||
field_buttons.select('button').remove();
|
||||
field_buttons.append('button')
|
||||
.call(svgIcon('#iD-icon-out-link'))
|
||||
.attr('class', 'form-field-button foreign-id-permalink')
|
||||
.attr('title', () => t('icons.visit_website'))
|
||||
.attr('aria-label', () => t('icons.visit_website'))
|
||||
.on('click', function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
window.open(clean_value, '_blank');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (d.display) {
|
||||
d.display(text_span);
|
||||
return;
|
||||
}
|
||||
text_span.text(d.value);
|
||||
});
|
||||
|
||||
chips.select('a')
|
||||
chips.select('a.remove')
|
||||
.attr('href', '#')
|
||||
.on('click', removeMultikey)
|
||||
.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)
|
||||
@@ -711,7 +769,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['⌦'])) {
|
||||
|
||||
@@ -732,6 +790,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) {
|
||||
|
||||
@@ -13,6 +13,15 @@ export function uiFieldDirectionalCombo(field, context) {
|
||||
|
||||
var _combos = {};
|
||||
|
||||
// fallback for schema-builder v5's cycleway field type: can be removed eventually
|
||||
if (field.type === 'cycleway') {
|
||||
field = {
|
||||
...field,
|
||||
key: field.keys[0],
|
||||
keys: field.keys.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
function directionalCombo(selection) {
|
||||
|
||||
function stripcolon(s) {
|
||||
@@ -37,10 +46,8 @@ export function uiFieldDirectionalCombo(field, context) {
|
||||
.attr('class', 'rows')
|
||||
.merge(div);
|
||||
|
||||
var keys = field.keys.slice(1);
|
||||
|
||||
items = div.selectAll('li')
|
||||
.data(keys);
|
||||
.data(field.keys);
|
||||
|
||||
var enter = items.enter()
|
||||
.append('li')
|
||||
@@ -77,8 +84,8 @@ export function uiFieldDirectionalCombo(field, context) {
|
||||
|
||||
|
||||
function change(key, newValue) {
|
||||
const commonKey = field.keys[0];
|
||||
const otherKey = key === field.keys[1] ? field.keys[2] : field.keys[1];
|
||||
const commonKey = field.key;
|
||||
const otherKey = key === field.keys[0] ? field.keys[1] : field.keys[0];
|
||||
|
||||
dispatch.call('change', this, tags => {
|
||||
const otherValue = tags[otherKey] || tags[commonKey];
|
||||
@@ -101,7 +108,7 @@ export function uiFieldDirectionalCombo(field, context) {
|
||||
directionalCombo.tags = function(tags) {
|
||||
_tags = tags;
|
||||
|
||||
const commonKey = field.keys[0];
|
||||
const commonKey = field.key;
|
||||
for (let key in _combos) {
|
||||
const uniqueValues = [... new Set([]
|
||||
.concat(_tags[commonKey])
|
||||
|
||||
@@ -63,6 +63,7 @@ export var uiFields = {
|
||||
colour: uiFieldColour,
|
||||
combo: uiFieldCombo,
|
||||
cycleway: uiFieldDirectionalCombo,
|
||||
date: uiFieldText,
|
||||
defaultCheck: uiFieldDefaultCheck,
|
||||
directionalCombo: uiFieldDirectionalCombo,
|
||||
email: uiFieldEmail,
|
||||
|
||||
+214
-49
@@ -1,15 +1,17 @@
|
||||
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';
|
||||
import { t, localizer } from '../../core/localizer';
|
||||
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';
|
||||
import { utilDetect, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';
|
||||
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,
|
||||
@@ -17,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');
|
||||
@@ -31,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')
|
||||
@@ -131,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
|
||||
@@ -151,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()();
|
||||
@@ -185,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');
|
||||
@@ -208,38 +219,44 @@ export function uiFieldText(field, context) {
|
||||
input.attr('type', 'text');
|
||||
|
||||
updateColourPreview();
|
||||
} else if (field.type === 'date') {
|
||||
input.attr('type', 'text');
|
||||
|
||||
updateDateField();
|
||||
}
|
||||
}
|
||||
|
||||
function isColourValid(colour) {
|
||||
if (!colour.match(/^(#([0-9a-fA-F]{3}){1,2}|\w+)$/)) {
|
||||
// OSM only supports hex or named colors
|
||||
return false;
|
||||
} else if (!CSS.supports('color', colour) || ['unset', 'inherit', 'initial', 'revert'].includes(colour)) {
|
||||
// see https://stackoverflow.com/a/68217760/1627467
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateColourPreview() {
|
||||
function isColourValid(colour) {
|
||||
if (!colour.match(/^(#([0-9a-fA-F]{3}){1,2}|\w+)$/)) {
|
||||
// OSM only supports hex or named colors
|
||||
return false;
|
||||
} else if (!CSS.supports('color', colour) || ['unset', 'inherit', 'initial', 'revert'].includes(colour)) {
|
||||
// see https://stackoverflow.com/a/68217760/1627467
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
wrap.selectAll('.colour-preview')
|
||||
.remove();
|
||||
|
||||
const colour = utilGetSetValue(input);
|
||||
|
||||
if (!isColourValid(colour) && colour !== '') return;
|
||||
if (!isColourValid(colour) && colour !== '') {
|
||||
wrap.selectAll('input.colour-selector').remove();
|
||||
wrap.selectAll('.form-field-button').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
var colourSelector = wrap.selectAll('.colour-selector')
|
||||
.data([0]);
|
||||
outlinkButton = wrap.selectAll('.colour-preview')
|
||||
.data([colour]);
|
||||
|
||||
colourSelector
|
||||
.enter()
|
||||
.append('input')
|
||||
.attr('type', 'color')
|
||||
.attr('class', 'form-field-button colour-selector')
|
||||
.attr('value', colour)
|
||||
.attr('class', 'colour-selector')
|
||||
.on('input', _debounce(function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
var colour = this.value;
|
||||
@@ -248,8 +265,12 @@ export function uiFieldText(field, context) {
|
||||
change()();
|
||||
updateColourPreview();
|
||||
}, 100));
|
||||
wrap.selectAll('input.colour-selector')
|
||||
.attr('value', colour);
|
||||
|
||||
outlinkButton = outlinkButton
|
||||
var chooserButton = wrap.selectAll('.colour-preview')
|
||||
.data([colour]);
|
||||
chooserButton = chooserButton
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'form-field-button colour-preview')
|
||||
@@ -257,12 +278,80 @@ export function uiFieldText(field, context) {
|
||||
.style('background-color', d => d)
|
||||
.attr('class', 'colour-box');
|
||||
if (colour === '') {
|
||||
outlinkButton = outlinkButton
|
||||
chooserButton = chooserButton
|
||||
.call(svgIcon('#iD-icon-edit'));
|
||||
}
|
||||
outlinkButton
|
||||
.on('click', () => wrap.select('.colour-selector').node().click())
|
||||
.merge(outlinkButton);
|
||||
chooserButton
|
||||
.on('click', () => wrap.select('.colour-selector').node().showPicker());
|
||||
}
|
||||
|
||||
|
||||
function updateDateField() {
|
||||
function isDateValid(date) {
|
||||
return date.match(/^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$/);
|
||||
}
|
||||
|
||||
const date = utilGetSetValue(input);
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().split('T')[0];
|
||||
if ((field.key === 'check_date' || field.key === 'survey:date') && date !== today) {
|
||||
wrap.selectAll('.date-set-today')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('button')
|
||||
.attr('class', 'form-field-button date-set-today')
|
||||
.call(svgIcon('#fas-rotate'))
|
||||
.call(uiTooltip().title(() => t.append('inspector.set_today')))
|
||||
.on('click', () => {
|
||||
utilGetSetValue(input, today);
|
||||
change()();
|
||||
updateDateField();
|
||||
});
|
||||
} else {
|
||||
wrap.selectAll('.date-set-today').remove();
|
||||
}
|
||||
|
||||
if (!isDateValid(date) && date !== '') {
|
||||
wrap.selectAll('input.date-selector').remove();
|
||||
wrap.selectAll('.date-calendar').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (utilDetect().browser !== 'Safari') {
|
||||
// opening of the calendar pick is not yet supported in safari <= 16
|
||||
// https://caniuse.com/mdn-api_htmlinputelement_showpicker_date_input
|
||||
|
||||
var dateSelector = wrap.selectAll('.date-selector')
|
||||
.data([0]);
|
||||
|
||||
dateSelector
|
||||
.enter()
|
||||
.append('input')
|
||||
.attr('type', 'date')
|
||||
.attr('class', 'date-selector')
|
||||
.on('input', _debounce(function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
var date = this.value;
|
||||
if (!isDateValid(date)) return;
|
||||
utilGetSetValue(input, this.value);
|
||||
change()();
|
||||
updateDateField();
|
||||
}, 100));
|
||||
wrap.selectAll('input.date-selector')
|
||||
.attr('value', date);
|
||||
|
||||
var calendarButton = wrap.selectAll('.date-calendar')
|
||||
.data([date]);
|
||||
calendarButton = calendarButton
|
||||
.enter()
|
||||
.append('button')
|
||||
.attr('class', 'form-field-button date-calendar')
|
||||
.call(svgIcon('#fas-calendar-days'));
|
||||
|
||||
calendarButton
|
||||
.on('click', () => wrap.select('.date-selector').node().showPicker());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,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;
|
||||
}
|
||||
@@ -305,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 = {};
|
||||
@@ -312,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,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);
|
||||
|
||||
@@ -362,7 +525,9 @@ export function uiFieldText(field, context) {
|
||||
|
||||
if (field.type === 'tel') updatePhonePlaceholder();
|
||||
|
||||
if (field.key.split(':').includes('colour')) updateColourPreview();
|
||||
if (field.type === 'colour') updateColourPreview();
|
||||
|
||||
if (field.type === 'date') updateDateField();
|
||||
|
||||
if (outlinkButton && !outlinkButton.empty()) {
|
||||
var disabled = !validIdentifierValueForLink();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { marked } from 'marked';
|
||||
import {
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
@@ -689,5 +690,10 @@ export function uiInit(context) {
|
||||
_saveLoading = d3_select(null);
|
||||
});
|
||||
|
||||
marked.use({
|
||||
mangle: false,
|
||||
headerIds: false,
|
||||
});
|
||||
|
||||
return ui;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export function uiPaneHelp(context) {
|
||||
'before_start',
|
||||
'open_source_h',
|
||||
'open_source',
|
||||
'open_source_attribution',
|
||||
'open_source_help'
|
||||
]],
|
||||
['overview', [
|
||||
|
||||
@@ -24,6 +24,8 @@ export function uiPhotoviewer(context) {
|
||||
if (services.streetside) { services.streetside.hideViewer(context); }
|
||||
if (services.mapillary) { services.mapillary.hideViewer(context); }
|
||||
if (services.kartaview) { services.kartaview.hideViewer(context); }
|
||||
if (services.mapilio) { services.mapilio.hideViewer(context); }
|
||||
if (services.vegbilder) { services.vegbilder.hideViewer(context); }
|
||||
})
|
||||
.append('div')
|
||||
.call(svgIcon('#iD-icon-close'));
|
||||
@@ -91,7 +93,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) {
|
||||
@@ -150,9 +152,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');
|
||||
}
|
||||
|
||||
@@ -298,7 +298,8 @@ export function uiPresetIcon() {
|
||||
const isMaki = picon && /^maki-/.test(picon);
|
||||
const isTemaki = picon && /^temaki-/.test(picon);
|
||||
const isFa = picon && /^fa[srb]-/.test(picon);
|
||||
const isiDIcon = picon && !(isMaki || isTemaki || isFa);
|
||||
const isRöntgen = picon && /^roentgen-/.test(picon);
|
||||
const isiDIcon = picon && !(isMaki || isTemaki || isFa || isRöntgen);
|
||||
|
||||
let icon = container.selectAll('.preset-icon')
|
||||
.data(picon ? [0] : []);
|
||||
@@ -368,6 +369,7 @@ export function uiPresetIcon() {
|
||||
subway: ['railway/subway', 'railway/subway', 'railway/subway'],
|
||||
train: ['railway/rail', 'railway/rail', 'railway/rail'],
|
||||
tram: ['railway/tram', 'railway/tram', 'railway/tram'],
|
||||
railway: ['railway/rail', 'railway/rail', 'railway/rail'],
|
||||
waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function uiSectionDataLayers(context) {
|
||||
var settingsCustomData = uiSettingsCustomData(context)
|
||||
.on('change', customChanged);
|
||||
|
||||
// refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}
|
||||
var layers = context.layers();
|
||||
|
||||
var section = uiSection('data-layers', context)
|
||||
@@ -386,7 +387,6 @@ export function uiSectionDataLayers(context) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawPanelItems(selection) {
|
||||
|
||||
var panelsListEnter = selection.selectAll('.md-extras-list')
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import {
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
import _debounce from 'lodash-es/debounce';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { t } from '../../core/localizer';
|
||||
import { localizer, t } from '../../core/localizer';
|
||||
import { uiTooltip } from '../tooltip';
|
||||
import { uiSection } from '../section';
|
||||
import { utilGetSetValue, utilNoAuto } from '../../util';
|
||||
import { uiSettingsLocalPhotos } from '../settings/local_photos';
|
||||
import { svgIcon } from '../../svg';
|
||||
|
||||
export function uiSectionPhotoOverlays(context) {
|
||||
|
||||
var settingsLocalPhotos = uiSettingsLocalPhotos(context)
|
||||
.on('change', localPhotosChanged);
|
||||
|
||||
var layers = context.layers();
|
||||
|
||||
var section = uiSection('photo-overlays', context)
|
||||
@@ -27,13 +31,21 @@ export function uiSectionPhotoOverlays(context) {
|
||||
.call(drawPhotoItems)
|
||||
.call(drawPhotoTypeItems)
|
||||
.call(drawDateFilter)
|
||||
.call(drawUsernameFilter);
|
||||
.call(drawUsernameFilter)
|
||||
.call(drawLocalPhotos);
|
||||
}
|
||||
|
||||
function drawPhotoItems(selection) {
|
||||
var photoKeys = context.photos().overlayLayerIDs();
|
||||
var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });
|
||||
var data = photoLayers.filter(function(obj) { return obj.layer.supported(); });
|
||||
var data = photoLayers.filter(function(obj) {
|
||||
if (!obj.layer.supported()) return false;
|
||||
if (layerEnabled(obj)) return true;
|
||||
if (typeof obj.layer.validHere === 'function') {
|
||||
return obj.layer.validHere(context.map().extent(), context.map().zoom());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function layerSupported(d) {
|
||||
return d.layer && d.layer.supported();
|
||||
@@ -41,6 +53,9 @@ export function uiSectionPhotoOverlays(context) {
|
||||
function layerEnabled(d) {
|
||||
return layerSupported(d) && d.layer.enabled();
|
||||
}
|
||||
function layerRendered(d) {
|
||||
return d.layer.rendered?.(context.map().zoom()) ?? true;
|
||||
}
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-photos')
|
||||
@@ -77,7 +92,13 @@ export function uiSectionPhotoOverlays(context) {
|
||||
else titleID = d.id.replace(/-/g, '_') + '.tooltip';
|
||||
d3_select(this)
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append(titleID))
|
||||
.title(() => {
|
||||
if (!layerRendered(d)) {
|
||||
return t.append('street_side.minzoom_tooltip');
|
||||
} else {
|
||||
return t.append(titleID);
|
||||
}
|
||||
})
|
||||
.placement('top')
|
||||
);
|
||||
});
|
||||
@@ -100,6 +121,7 @@ export function uiSectionPhotoOverlays(context) {
|
||||
.merge(liEnter)
|
||||
.classed('active', layerEnabled)
|
||||
.selectAll('input')
|
||||
.property('disabled', d => !layerRendered(d))
|
||||
.property('checked', layerEnabled);
|
||||
}
|
||||
|
||||
@@ -317,8 +339,106 @@ export function uiSectionPhotoOverlays(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function drawLocalPhotos(selection) {
|
||||
var photoLayer = layers.layer('local-photos');
|
||||
var hasData = photoLayer && photoLayer.hasData();
|
||||
var showsData = hasData && photoLayer.enabled();
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-local-photos')
|
||||
.data(photoLayer ? [0] : []);
|
||||
|
||||
// Exit
|
||||
ul.exit()
|
||||
.remove();
|
||||
|
||||
// Enter
|
||||
var ulEnter = ul.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list layer-list-local-photos');
|
||||
|
||||
var localPhotosEnter = ulEnter
|
||||
.append('li')
|
||||
.attr('class', 'list-item-local-photos');
|
||||
|
||||
var localPhotosLabelEnter = localPhotosEnter
|
||||
.append('label')
|
||||
.call(uiTooltip().title(() => t.append('local_photos.tooltip')));
|
||||
|
||||
localPhotosLabelEnter
|
||||
.append('input')
|
||||
.attr('type', 'checkbox')
|
||||
.on('change', function() { toggleLayer('local-photos'); });
|
||||
|
||||
localPhotosLabelEnter
|
||||
.call(t.append('local_photos.header'));
|
||||
|
||||
localPhotosEnter
|
||||
.append('button')
|
||||
.attr('class', 'open-data-options')
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.tooltip_edit'))
|
||||
.placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')
|
||||
)
|
||||
.on('click', function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
editLocalPhotos();
|
||||
})
|
||||
.call(svgIcon('#iD-icon-more'));
|
||||
|
||||
localPhotosEnter
|
||||
.append('button')
|
||||
.attr('class', 'zoom-to-data')
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.zoom'))
|
||||
.placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')
|
||||
)
|
||||
.on('click', function(d3_event) {
|
||||
if (d3_select(this).classed('disabled')) return;
|
||||
|
||||
d3_event.preventDefault();
|
||||
d3_event.stopPropagation();
|
||||
photoLayer.fitZoom();
|
||||
})
|
||||
.call(svgIcon('#iD-icon-framed-dot', 'monochrome'));
|
||||
|
||||
// Update
|
||||
ul = ul
|
||||
.merge(ulEnter);
|
||||
|
||||
ul.selectAll('.list-item-local-photos')
|
||||
.classed('active', showsData)
|
||||
.selectAll('label')
|
||||
.classed('deemphasize', !hasData)
|
||||
.selectAll('input')
|
||||
.property('disabled', !hasData)
|
||||
.property('checked', showsData);
|
||||
|
||||
ul.selectAll('button.zoom-to-data')
|
||||
.classed('disabled', !hasData);
|
||||
}
|
||||
|
||||
function editLocalPhotos() {
|
||||
context.container()
|
||||
.call(settingsLocalPhotos);
|
||||
}
|
||||
|
||||
function localPhotosChanged(d) {
|
||||
var localPhotosLayer = layers.layer('local-photos');
|
||||
|
||||
localPhotosLayer.fileList(d);
|
||||
}
|
||||
|
||||
context.layers().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
context.photos().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
|
||||
context.map()
|
||||
.on('move.photo_overlays',
|
||||
_debounce(function() {
|
||||
// layers in-view may have changed due to map move
|
||||
window.requestIdleCallback(section.reRender);
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -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,8 @@ 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';
|
||||
import { fileFetcher } from '../../core';
|
||||
|
||||
|
||||
export function uiSectionRawTagEditor(id, context) {
|
||||
@@ -31,6 +33,11 @@ export function uiSectionRawTagEditor(id, context) {
|
||||
{ id: 'text', icon: '#fas-i-cursor' }
|
||||
];
|
||||
|
||||
let _discardTags = {};
|
||||
fileFetcher.get('discarded')
|
||||
.then((d) => { _discardTags = d; })
|
||||
.catch(() => { /* ignore */ });
|
||||
|
||||
var _tagView = (prefs('raw-tag-editor-view') || 'list'); // 'list, 'text'
|
||||
var _readOnlyTags = [];
|
||||
// the keys in the order we want them to display
|
||||
@@ -285,7 +292,11 @@ export function uiSectionRawTagEditor(id, context) {
|
||||
});
|
||||
|
||||
items.selectAll('button.remove')
|
||||
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', removeTag); // 'click' fires too late - #5878
|
||||
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878
|
||||
(d3_event, d) => {
|
||||
if (d3_event.button !== 0) return;
|
||||
removeTag(d3_event, d);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -421,7 +432,10 @@ 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 in _discardTags)) // do not suggest discardable tags (see #9817)
|
||||
.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));
|
||||
callback(sort(value, filtered));
|
||||
}
|
||||
});
|
||||
@@ -435,9 +449,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) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export function uiSettingsCustomBackground() {
|
||||
template: prefs('background-custom-template')
|
||||
};
|
||||
|
||||
var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
|
||||
var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';
|
||||
var modal = uiConfirm(selection).okButton();
|
||||
|
||||
modal
|
||||
|
||||
@@ -19,10 +19,10 @@ export function uiSettingsCustomData(context) {
|
||||
};
|
||||
var _currSettings = {
|
||||
fileList: (dataLayer && dataLayer.fileList()) || null,
|
||||
url: prefs('settings-custom-data-url')
|
||||
// url: prefs('settings-custom-data-url')
|
||||
};
|
||||
|
||||
// var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
|
||||
// var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';
|
||||
var modal = uiConfirm(selection).okButton();
|
||||
|
||||
modal
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { isArray, isNumber } from 'lodash-es';
|
||||
|
||||
import { t } from '../../core/localizer';
|
||||
import { uiConfirm } from '../confirm';
|
||||
import { utilRebind } from '../../util';
|
||||
import { uiTooltip } from '../tooltip';
|
||||
import { svgIcon } from '../../svg';
|
||||
|
||||
|
||||
export function uiSettingsLocalPhotos(context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var photoLayer = context.layers().layer('local-photos');
|
||||
var modal;
|
||||
|
||||
function render(selection) {
|
||||
|
||||
modal = uiConfirm(selection).okButton();
|
||||
|
||||
modal
|
||||
.classed('settings-modal settings-local-photos', true);
|
||||
|
||||
modal.select('.modal-section.header')
|
||||
.append('h3')
|
||||
.call(t.append('local_photos.header'));
|
||||
|
||||
modal.select('.modal-section.message-text')
|
||||
.append('div')
|
||||
.classed('local-photos', true);
|
||||
|
||||
var instructionsSection = modal.select('.modal-section.message-text .local-photos')
|
||||
.append('div')
|
||||
.classed('instructions', true);
|
||||
|
||||
instructionsSection
|
||||
.append('p')
|
||||
.classed('instructions-local-photos', true)
|
||||
.call(t.append('local_photos.file.instructions'));
|
||||
|
||||
instructionsSection
|
||||
.append('input')
|
||||
.classed('field-file', true)
|
||||
.attr('type', 'file')
|
||||
.attr('multiple', 'multiple')
|
||||
.attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')
|
||||
.style('visibility', 'hidden')
|
||||
.attr('id', 'local-photo-files')
|
||||
.on('change', function(d3_event) {
|
||||
var files = d3_event.target.files;
|
||||
if (files && files.length) {
|
||||
photoList
|
||||
.select('ul')
|
||||
.append('li')
|
||||
.classed('placeholder', true)
|
||||
.append('div');
|
||||
dispatch.call('change', this, files);
|
||||
}
|
||||
d3_event.target.value = null;
|
||||
});
|
||||
instructionsSection
|
||||
.append('label')
|
||||
.attr('for', 'local-photo-files')
|
||||
.classed('button', true)
|
||||
.call(t.append('local_photos.file.label'));
|
||||
|
||||
const photoList = modal.select('.modal-section.message-text .local-photos')
|
||||
.append('div')
|
||||
.append('div')
|
||||
.classed('list-local-photos', true);
|
||||
|
||||
photoList
|
||||
.append('ul');
|
||||
|
||||
updatePhotoList(photoList.select('ul'));
|
||||
|
||||
context.layers().on('change', () => updatePhotoList(photoList.select('ul')));
|
||||
}
|
||||
|
||||
function updatePhotoList(container) {
|
||||
function locationUnavailable(d) {
|
||||
return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));
|
||||
}
|
||||
|
||||
container.selectAll('li.placeholder').remove();
|
||||
|
||||
let selection = container.selectAll('li')
|
||||
.data(photoLayer.getPhotos() ?? [], d => d.id);
|
||||
selection.exit()
|
||||
.remove();
|
||||
|
||||
const selectionEnter = selection.enter()
|
||||
.append('li');
|
||||
|
||||
selectionEnter
|
||||
.append('span')
|
||||
.classed('filename', true);
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button zoom-to-data', true)
|
||||
.attr('title', t('local_photos.zoom_single'))
|
||||
.call(svgIcon('#iD-icon-framed-dot'));
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button no-geolocation', true)
|
||||
.call(svgIcon('#iD-icon-alert'))
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.no_geolocation.tooltip'))
|
||||
.placement('left')
|
||||
);
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button remove', true)
|
||||
.attr('title', t('icons.remove'))
|
||||
.call(svgIcon('#iD-operation-delete'));
|
||||
|
||||
selection = selection.merge(selectionEnter);
|
||||
|
||||
selection
|
||||
.classed('invalid', locationUnavailable);
|
||||
selection.select('span.filename')
|
||||
.text(d => d.name)
|
||||
.attr('title', d => d.name);
|
||||
selection.select('span.filename')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.openPhoto(d3_event, d, false);
|
||||
});
|
||||
selection.select('button.zoom-to-data')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.openPhoto(d3_event, d, true);
|
||||
});
|
||||
selection.select('button.remove')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.removePhoto(d.id);
|
||||
updatePhotoList(container);
|
||||
});
|
||||
}
|
||||
|
||||
return utilRebind(render, dispatch, 'on');
|
||||
}
|
||||
Reference in New Issue
Block a user