diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5a290f1..d1bff6662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,10 +49,13 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :earth_asia: Localization #### :hourglass: Performance #### :mortar_board: Walkthrough / Help +#### :rocket: Presets +* Suggest housenumber/housename values from surrounding areas ([#10946]) #### :hammer: Development [#9873]: https://github.com/openstreetmap/iD/issues/9873 [#10104]: https://github.com/openstreetmap/iD/issues/10104 +[#10946]: https://github.com/openstreetmap/iD/issues/10946 [#10959]: https://github.com/openstreetmap/iD/issues/10959 [#10966]: https://github.com/openstreetmap/iD/issues/10966 diff --git a/modules/ui/fields/address.js b/modules/ui/fields/address.js index 6537794eb..2b7851215 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -4,7 +4,7 @@ import * as countryCoder from '@rapideditor/country-coder'; import { presetManager } from '../../presets'; import { fileFetcher } from '../../core/file_fetcher'; -import { geoExtent, geoChooseEdge, geoSphericalDistance } from '../../geo'; +import { geoChooseEdge, geoSphericalDistance, geoPolygonContainsPolygon, geoPointInPolygon } from '../../geo'; import { uiCombobox } from '../combobox'; import { utilArrayUniqBy, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilTriggerEvent } from '../../util'; import { t } from '../../core/localizer'; @@ -39,7 +39,7 @@ export function uiFieldAddress(field, context) { function getNear(isAddressable, type, searchRadius, resultProp) { var extent = combinedEntityExtent(); var l = extent.center(); - var box = geoExtent(l).padByMeters(searchRadius); + var box = extent.padByMeters(searchRadius); var features = context.history().intersects(box) .filter(isAddressable) @@ -77,6 +77,35 @@ export function uiFieldAddress(field, context) { return utilArrayUniqBy(features, 'value'); } + function getEnclosing(isAddressable, type, resultProp) { + var extent = combinedEntityExtent(); + + var features = context.history().intersects(extent) + .filter(isAddressable) + .map(d => { + if (d.geometry(context.graph()) !== 'area') { + return false; + } + + const geom = d.asGeoJSON(context.graph()).coordinates[0]; + if (!geoPolygonContainsPolygon(geom, extent.polygon())) { + return false; + } + + const value = resultProp && d.tags[resultProp] ? d.tags[resultProp] : d.tags.name; + return { + title: value, + value, + dist: 0, + geom, + type, + klass: `address-${type}` + }; + }).filter(Boolean); + + return utilArrayUniqBy(features, 'value'); + } + function getNearStreets() { function isAddressable(d) { return d.tags.highway && d.tags.name && d.type === 'way'; @@ -130,6 +159,37 @@ export function uiFieldAddress(field, context) { return getNear(hasTag, key, 200, tagKey); } + function getEnclosingValues(key) { + const tagKey = `${field.key}:${key}`; + + // 1. areas encompassing the feature that have the address tag + function hasTag(d) { + return _entityIDs.indexOf(d.id) === -1 && d.tags[tagKey]; + } + const enclosingAddresses = getEnclosing(hasTag, key, tagKey); + + // 2. also include addresses from points which are encompassed by + // the same building area as the current feature + function isBuilding(d) { + return _entityIDs.indexOf(d.id) === -1 && d.tags.building && d.tags.building !== 'no'; + } + const enclosingBuildings = getEnclosing(isBuilding, 'building', 'building').map(d => d.geom); + function isInNearbyBuilding(d) { + return hasTag(d) && + d.type === 'node' && + enclosingBuildings.some(geom => + geoPointInPolygon(d.loc, geom) || + geom.indexOf(d.loc) !== -1 + ); + } + const nearPointAddresses = getNear(isInNearbyBuilding, key, 100, tagKey); + + return utilArrayUniqBy([ + ...enclosingAddresses, + ...nearPointAddresses + ], 'value').sort((a, b) => a.value > b.value ? 1 : -1); + } + function updateForCountryCode() { @@ -146,6 +206,10 @@ export function uiFieldAddress(field, context) { } } + const maybeDropdowns = new Set([ + 'housenumber', + 'housename' + ]); const dropdowns = new Set([ 'block_number', 'city', @@ -164,7 +228,8 @@ export function uiFieldAddress(field, context) { 'street+place', 'subdistrict', 'suburb', - 'town' + 'town', + ...maybeDropdowns ]); var widths = addressFormat.widths || { @@ -211,7 +276,9 @@ export function uiFieldAddress(field, context) { function addDropdown(d) { - if (!dropdowns.has(d.id)) return; // not a dropdown + if (!dropdowns.has(d.id)) { + return false; // not a dropdown + } var nearValues; switch (d.id) { @@ -234,10 +301,23 @@ export function uiFieldAddress(field, context) { case 'postcode': nearValues = getNearPostcodes; break; + case 'housenumber': + case 'housename': + nearValues = getEnclosingValues; + break; default: nearValues = getNearValues; } + if (maybeDropdowns.has(d.id)) { + const candidates = nearValues(d.id); + // only add dropdown if there are possible values for the + // corresponding tag: e.g. only show ▼ caret for + // housenumber/housename if the feature is actually + // encompassed by another feature with such an address + if (candidates.length === 0) return false; + } + d3_select(this) .call(uiCombobox(context, `address-${d.isAutoStreetPlace ? 'street-place' : d.id}`) .minItems(1)