diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e61d4326..9216e31f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :sparkles: Usability & Accessibility * Make it easier to search for OSM objects by id ([#9520], thanks [@k-yle]) * Localize numbers in numeric fields ([#8769], thanks [@1ec5]) +* The Address field now supports the `addr:place` tag (as an alternative to `addr:street`), this functionality is activated in selected countries ([#9603]) #### :scissors: Operations #### :camera: Street-Level #### :white_check_mark: Validation @@ -65,7 +66,7 @@ _Breaking developer changes, which may affect downstream projects or sites that * Render "right-side" arrows for features with lifecycle prefixes ([#9493], thanks [@k-yle]) * Take regional variants of parent presets into account when resolving preset fields ([#9524]) * Render "right-side" arrows for `man_made=quay` features -* Add support icons also in `multiCombo` and `semiCombo` fields ([#9433]) +* Support icons also in `multiCombo` and `semiCombo` fields ([#9433]) #### :hammer: Development * Upgrade dependencies: `fortawesome` to v6.4, `which-polygon` to v2.2.1, `glob` to v9.2, `temaki` to v5.4, `marked` to v4.3, `core-js-bundle` to v3.30, `osm-auth` to v2.1 * Bundle `package-lock.json` file in repository for faster `clean-install` builds @@ -82,6 +83,7 @@ _Breaking developer changes, which may affect downstream projects or sites that [#9501]: https://github.com/openstreetmap/iD/pull/9501 [#9520]: https://github.com/openstreetmap/iD/pull/9520 [#9524]: https://github.com/openstreetmap/iD/issues/9524 +[#9603]: https://github.com/openstreetmap/iD/pull/9603 [#9630]: https://github.com/openstreetmap/iD/pull/9630 [#9637]: https://github.com/openstreetmap/iD/pull/9637 [#9638]: https://github.com/openstreetmap/iD/pull/9638 diff --git a/css/80_app.css b/css/80_app.css index 9fbf2f341..8121a1b50 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2128,6 +2128,19 @@ input.date-selector { .ideditor[dir='rtl'] .addr-row:last-of-type input:last-of-type { border-radius: 0 0 0 4px; } +.combobox-address-street-place .combobox-option.address-street, +.combobox-address-street-place .combobox-option.address-place { + padding-right: 20px; +} +.combobox-address-street-place .combobox-option.address-street::after, +.combobox-address-street-place .combobox-option.address-place::after { + position: absolute; + right: 2px; + opacity: 0.4; +} +.combobox-address-street-place .combobox-option.address-place::after { + content: url(data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB2aWV3Qm94PSIwIDAgMjAgMjAiPg0KICAgIDxwYXRoIGQ9Ik0xMCwzIEM2LjY4NiwzIDQsNS42MTIgNCw4LjgzMyBDNCwxMi4wNTUgMTAsMTcgMTAsMTcgQzEwLDE3IDE2LDEyLjA1NSAxNiw4LjgzMyBDMTYsNS42MTIgMTMuMzE0LDMgMTAsMyB6IE0xMC4xODcsNi41IEMxMS41MTMsNi41IDEyLjU4Nyw3LjU0NSAxMi41ODcsOC44MzMgQzEyLjU4NywxMC4xMjIgMTEuNTEzLDExLjE2NyAxMC4xODcsMTEuMTY3IEM4Ljg2MiwxMS4xNjcgNy43ODcsMTAuMTIyIDcuNzg3LDguODMzIEM3Ljc4Nyw3LjU0NSA4Ljg2Miw2LjUgMTAuMTg3LDYuNSB6IiBmaWxsPSJjdXJyZW50Q29sb3IiLz4NCjwvc3ZnPg==); +} /* Field - Wikipedia diff --git a/data/address_formats.json b/data/address_formats.json index cf02c2df3..8c0872440 100644 --- a/data/address_formats.json +++ b/data/address_formats.json @@ -1,7 +1,7 @@ [ { "format": [ - ["housenumber", "street"], + ["housenumber", "street+place"], ["city", "postcode"] ] }, @@ -16,7 +16,7 @@ "countryCodes": ["gb"], "format": [ ["housename"], - ["housenumber", "street"], + ["housenumber", "street+place"], ["city", "postcode"] ] }, @@ -24,22 +24,31 @@ "countryCodes": ["ie"], "format": [ ["housename"], - ["housenumber", "street"], + ["housenumber", "street+place"], ["city"], ["postcode"] ] }, { - "countryCodes": ["at", "bg", "ch", "de", "si", "pl"], + "countryCodes": ["at", "bg", "ch", "de", "si", "pl", "lt"], "format": [ - ["street", "housenumber"], + ["street+place", "housenumber"], ["postcode", "city"] ] }, { "countryCodes": [ - "ad", "ba", "be", "cz", "dk", "es", "fi", "gr", "hr", "is", - "it", "li", "nl", "no", "pt", "se", "sk", "sm", "va" + "ad", "ba", "be", "dk", "es", "gr", "hr", + "it", "pt", "se", "sm", "va" + ], + "format": [ + ["street+place", "housenumber", "unit"], + ["postcode", "city"] + ] + }, + { + "countryCodes": [ + "fi", "is", "li", "nl", "no" ], "format": [ ["street", "housenumber", "unit"], @@ -47,16 +56,30 @@ ] }, { - "countryCodes": ["fr", "lu", "mo"], + "countryCodes": ["fr", "lu"], + "format": [ + ["housenumber", "street+place"], + ["postcode", "city"] + ] + }, + { + "countryCodes": ["mo"], "format": [ ["housenumber", "street"], ["postcode", "city"] ] }, + { + "countryCodes": ["lv"], + "format": [ + ["street", "housenumber"], + ["city", "postcode"] + ] + }, { "countryCodes": ["br"], "format": [ - ["street"], + ["street+place"], ["housenumber", "suburb"], ["city", "postcode"] ] @@ -89,7 +112,7 @@ "countryCodes": ["tw"], "format": [ ["postcode", "city", "district"], - ["place", "street"], + ["street+place"], ["housenumber", "floor", "unit"] ] }, @@ -121,7 +144,7 @@ "countryCodes": ["tr"], "format": [ ["neighbourhood"], - ["street", "housenumber"], + ["street+place", "housenumber"], ["postcode", "district", "city"] ] }, @@ -187,6 +210,20 @@ ["district"] ] }, + { + "countryCodes": ["ru"], + "format": [ + ["housenumber", "street+place"], + ["city", "postcode"] + ] + }, + { + "countryCodes": ["cz", "sk"], + "format": [ + ["street", "housenumber"], + ["postcode", "city"] + ] + }, { "countryCodes": ["ph"], "format": [ diff --git a/modules/services/streetside.js b/modules/services/streetside.js index 5a5b37f7f..52af3c3ef 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -852,7 +852,7 @@ export default { bubbleIdQuadKey = '0' + bubbleIdQuadKey; } const imgUrlPrefix = streetsideImagesApi + 'hs' + bubbleIdQuadKey; - const imgUrlSuffix = '.jpg?g=13305&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']; diff --git a/modules/ui/fields/address.js b/modules/ui/fields/address.js index 005e8d624..e932571d8 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -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.type === 'way') { + 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 = Math.min(dist, choice.distance); + } + + 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) {