diff --git a/CHANGELOG.md b/CHANGELOG.md index c29ca951d..694279da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ _Breaking developer changes, which may affect downstream projects or sites that # unreleased (v2.35.0-dev) #### :sparkles: Usability & Accessibility +* Render housenumbers (or housenames) of address points or buildings as dedicated labels on the map ([#10970]) #### :scissors: Operations #### :camera: Street-Level #### :white_check_mark: Validation @@ -47,6 +48,8 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :mortar_board: Walkthrough / Help #### :hammer: Development +[#10970]: https://github.com/openstreetmap/iD/pull/10970 + # v2.34.0 ##### 2025-05-12 diff --git a/data/core.yaml b/data/core.yaml index f6300a3d5..9873cf07d 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -914,6 +914,9 @@ en: points: description: Points tooltip: "Points of Interest" + address_points: + description: Address Points + tooltip: "Addresses Mapped as Individual Points" traffic_roads: description: Traffic Roads tooltip: "Highways, Streets, etc." diff --git a/modules/actions/merge_remote_changes.js b/modules/actions/merge_remote_changes.js index 358f6a66b..01e940686 100644 --- a/modules/actions/merge_remote_changes.js +++ b/modules/actions/merge_remote_changes.js @@ -1,6 +1,6 @@ import deepEqual from 'fast-deep-equal'; import { diff3Merge } from 'node-diff3'; -import { escape } from 'lodash'; +import { escape } from 'lodash-es'; import { t } from '../core/localizer'; import { actionDeleteMultiple } from './delete_multiple'; diff --git a/modules/core/history.js b/modules/core/history.js index 1d019922b..e3ea7b324 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -80,12 +80,12 @@ export function coreHistory(context) { // internal _overwrite with eased time function _overwrite(args, t) { var previous = _stack[_index].graph; + var actionResult = _act(args, t); if (_index > 0) { _index--; _stack.pop(); } _stack = _stack.slice(0, _index + 1); - var actionResult = _act(args, t); _stack.push(actionResult); _index++; return change(previous); diff --git a/modules/modes/move.js b/modules/modes/move.js index a03728783..7a10961fe 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -47,7 +47,7 @@ export function modeMove(context, entityIDs, baseGraph) { var _prevGraph; var _cache; - var _origin; + var _prevMouse; var _nudgeInterval; // use pointer events on supported platforms; fallback to mouse events @@ -57,18 +57,18 @@ export function modeMove(context, entityIDs, baseGraph) { function doMove(nudge) { nudge = nudge || [0, 0]; - var fn; + let fn; if (_prevGraph !== context.graph()) { _cache = {}; - _origin = context.map().mouseCoordinates(); + _prevMouse = context.map().mouse(); fn = context.perform; } else { fn = context.overwrite; } - var currMouse = context.map().mouse(); - var origMouse = context.projection(_origin); - var delta = geoVecSubtract(geoVecSubtract(currMouse, origMouse), nudge); + const currMouse = context.map().mouse(); + const delta = geoVecSubtract(geoVecSubtract(currMouse, _prevMouse), nudge); + _prevMouse = currMouse; fn(actionMove(entityIDs, delta, context.projection, _cache)); _prevGraph = context.graph(); @@ -129,7 +129,7 @@ export function modeMove(context, entityIDs, baseGraph) { mode.enter = function() { - _origin = context.map().mouseCoordinates(); + _prevMouse = context.map().mouse(); _prevGraph = null; _cache = {}; diff --git a/modules/osm/tags.js b/modules/osm/tags.js index 959618dce..a7d0448a2 100644 --- a/modules/osm/tags.js +++ b/modules/osm/tags.js @@ -1,13 +1,37 @@ import { merge } from 'lodash-es'; +const uninterestingKeys = new Set([ + 'attribution', + 'created_by', + 'import_uuid', + 'geobase:datasetName', + 'geobase:uuid', + 'KSJ2:curve_id', + 'KSJ2:lat', + 'KSJ2:long', + 'lat', + 'latitude', + 'lon', + 'longitude', + 'source', + 'source_ref', + 'odbl', + 'odbl:note' +]); +const uninterestingKeyRegex = /^(source(_ref)?|tiger):/; + +/** + * Returns whether the given OSM tag key is potentially "interesting". + * For example, some tags are deemed not interesting because the respective tag is + * considered "discardable". + * + * @param {string} key the key to test + * @returns {boolean} + */ export function osmIsInterestingTag(key) { - return key !== 'attribution' && - key !== 'created_by' && - key !== 'source' && - key !== 'odbl' && - key.indexOf('source:') !== 0 && - key.indexOf('source_ref') !== 0 && // purposely exclude colon - key.indexOf('tiger:') !== 0; + if (uninterestingKeys.has(key)) return false; + if (uninterestingKeyRegex.test(key)) return false; + return true; } export const osmLifecyclePrefixes = { @@ -296,7 +320,7 @@ export function isColourValid(value) { } // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44 -export var osmMutuallyExclusiveTagPairs = [ +export const osmMutuallyExclusiveTagPairs = [ ['noname', 'name'], ['noref', 'ref'], ['nohousenumber', 'addr:housenumber'], diff --git a/modules/presets/preset.js b/modules/presets/preset.js index 4da3ccefd..8e6f68d2e 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -1,4 +1,4 @@ -import { isEqual } from 'lodash'; +import { isEqual } from 'lodash-es'; import { t } from '../core/localizer'; import { osmAreaKeys, osmAreaKeysExceptions } from '../osm/tags'; diff --git a/modules/renderer/background_source.js b/modules/renderer/background_source.js index 57bf26d8d..9d29779c3 100644 --- a/modules/renderer/background_source.js +++ b/modules/renderer/background_source.js @@ -1,6 +1,6 @@ import { geoArea as d3_geoArea, geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo'; import { json as d3_json } from 'd3-fetch'; -import { escape } from 'lodash'; +import { escape } from 'lodash-es'; import { t, localizer } from '../core/localizer'; import { geoExtent, geoSphericalDistance } from '../geo'; diff --git a/modules/renderer/features.js b/modules/renderer/features.js index 6514d0d13..f339c332e 100644 --- a/modules/renderer/features.js +++ b/modules/renderer/features.js @@ -1,7 +1,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { prefs } from '../core/preferences'; -import { osmEntity, osmLifecyclePrefixes } from '../osm'; +import { osmEntity, osmIsInterestingTag, osmLifecyclePrefixes } from '../osm'; import { utilRebind } from '../util/rebind'; import { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs } from '../util'; @@ -101,9 +101,18 @@ export function rendererFeatures(context) { }; } + function isAddressPoint(tags, geometry) { + const keys = Object.keys(tags); + return geometry === 'point' && + keys.length > 0 && + keys.every(key => + key.startsWith('addr:') || !osmIsInterestingTag(key) + ); + } + defineRule('address_points', isAddressPoint, 100); defineRule('points', function isPoint(tags, geometry) { - return geometry === 'point'; + return geometry === 'point' && !isAddressPoint(tags, geometry); }, 200); defineRule('traffic_roads', function isTrafficRoad(tags) { diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 6141f24ba..2c693aa2c 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -17,7 +17,7 @@ import { utilGetDimensions } from '../util/dimensions'; import { utilRebind } from '../util/rebind'; import { utilZoomPan } from '../util/zoom_pan'; import { utilDoubleUp } from '../util/double_up'; -import { isArray } from 'lodash-es'; +import { isArray, clamp } from 'lodash-es'; // constants var TILESIZE = 256; @@ -26,10 +26,6 @@ var maxZoom = 24; var kMin = geoZoomToScale(minZoom, TILESIZE); var kMax = geoZoomToScale(maxZoom, TILESIZE); -function clamp(num, min, max) { - return Math.max(min, Math.min(num, max)); -} - export function rendererMap(context) { var dispatch = d3_dispatch( @@ -393,8 +389,8 @@ export function rendererMap(context) { .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) - .call(drawLabels, graph, data, filter, _dimensions, fullRedraw) - .call(drawPoints, graph, data, filter); + .call(drawPoints, graph, data, filter) + .call(drawLabels, graph, data, filter, _dimensions, fullRedraw); dispatch.call('drawn', this, {full: true}); } diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index c47bea4d6..5bd3fe7e4 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -5,7 +5,7 @@ import { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom'; import Protobuf from 'pbf'; import RBush from 'rbush'; import { VectorTile } from '@mapbox/vector-tile'; -import { isEqual } from 'lodash'; +import { isEqual } from 'lodash-es'; import { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util'; import {geoExtent, geoScaleToZoom} from '../geo'; diff --git a/modules/svg/labels.js b/modules/svg/labels.js index fa0bcabd8..a80d29c9b 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -9,9 +9,9 @@ import { geoScaleToZoom, geoVecInterp, geoVecLength } from '../geo'; import { presetManager } from '../presets'; -import { osmEntity } from '../osm'; +import { osmEntity, osmIsInterestingTag } from '../osm'; import { utilDetect } from '../util/detect'; -import { utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util'; +import { utilArrayDifference, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util'; @@ -23,11 +23,10 @@ export function svgLabels(projection, context) { (detected.browser.toLowerCase() === 'firefox' && detected.version >= 70)); var _rdrawn = new RBush(); var _rskipped = new RBush(); - var _textWidthCache = {}; var _entitybboxes = {}; // Listed from highest to lowest priority - var labelStack = [ + const labelStack = [ ['line', 'aeroway', '*', 12], ['line', 'highway', 'motorway', 12], ['line', 'highway', 'trunk', 12], @@ -62,7 +61,9 @@ export function svgLabels(projection, context) { ['point', 'ref', '*', 10], ['line', 'name', '*', 12], ['area', 'name', '*', 12], - ['point', 'name', '*', 10] + ['point', 'name', '*', 10], + ['point', 'addr:housenumber', '*', 10], + ['point', 'addr:housename', '*', 10] ]; @@ -74,37 +75,10 @@ export function svgLabels(projection, context) { } - function get(array, prop) { - return function(d, i) { return array[i][prop]; }; - } - - - function textWidth(text, size, elem) { - var c = _textWidthCache[size]; - if (!c) c = _textWidthCache[size] = {}; - - if (c[text]) { - return c[text]; - - } else if (elem) { - c[text] = elem.getComputedTextLength(); - return c[text]; - - } else { - var str = encodeURIComponent(text).match(/%[CDEFcdef]/g); - if (str === null) { - return size / 3 * 2 * text.length; - } else { - return size / 3 * (2 * text.length + str.length); - } - } - } - - - function drawLinePaths(selection, entities, filter, classes, labels) { - var paths = selection.selectAll('path') - .filter(filter) - .data(entities, osmEntity.key); + function drawLinePaths(selection, labels, filter, classes) { + var paths = selection.selectAll('path:not(.debug)') + .filter(d => filter(d.entity)) + .data(labels, d => osmEntity.key(d.entity)); // exit paths.exit() @@ -113,18 +87,18 @@ export function svgLabels(projection, context) { // enter/update paths.enter() .append('path') - .style('stroke-width', get(labels, 'font-size')) - .attr('id', function(d) { return 'ideditor-labelpath-' + d.id; }) + .style('stroke-width', d => d.position['font-size']) + .attr('id', d => 'ideditor-labelpath-' + d.entity.id) .attr('class', classes) .merge(paths) - .attr('d', get(labels, 'lineString')); + .attr('d', d => d.position.lineString); } - function drawLineLabels(selection, entities, filter, classes, labels) { + function drawLineLabels(selection, labels, filter, classes) { var texts = selection.selectAll('text.' + classes) - .filter(filter) - .data(entities, osmEntity.key); + .filter(d => filter(d.entity)) + .data(labels, d => osmEntity.key(d.entity)); // exit texts.exit() @@ -133,25 +107,28 @@ export function svgLabels(projection, context) { // enter texts.enter() .append('text') - .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) + .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id) .attr('dy', baselineHack ? '0.35em' : null) .append('textPath') .attr('class', 'textpath'); // update selection.selectAll('text.' + classes).selectAll('.textpath') - .filter(filter) - .data(entities, osmEntity.key) + .filter(d => filter(d.entity)) + .data(labels, d => osmEntity.key(d.entity)) .attr('startOffset', '50%') - .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.id; }) - .text(utilDisplayNameForPath); + .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; }) + .text(d => d.name); } - function drawPointLabels(selection, entities, filter, classes, labels) { + function drawPointLabels(selection, labels, filter, classes) { + if (classes.includes('pointlabel-halo')) { + labels = labels.filter(d => !d.position.isAddr); + } var texts = selection.selectAll('text.' + classes) - .filter(filter) - .data(entities, osmEntity.key); + .filter(d => filter(d.entity)) + .data(labels, d => osmEntity.key(d.entity)); // exit texts.exit() @@ -160,35 +137,29 @@ export function svgLabels(projection, context) { // enter/update texts.enter() .append('text') - .attr('class', function(d, i) { - return classes + ' ' + labels[i].classes + ' ' + d.id; - }) + .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id) + .style('text-anchor', d => d.position.textAnchor) + .text(d => d.name) .merge(texts) - .attr('x', get(labels, 'x')) - .attr('y', get(labels, 'y')) - .style('text-anchor', get(labels, 'textAnchor')) - .text(utilDisplayName) - .each(function(d, i) { - textWidth(utilDisplayName(d), labels[i].height, this); - }); + .attr('x', d => d.position.x) + .attr('y', d => d.position.y); } - function drawAreaLabels(selection, entities, filter, classes, labels) { - entities = entities.filter(hasText); + function drawAreaLabels(selection, labels, filter, classes) { labels = labels.filter(hasText); - drawPointLabels(selection, entities, filter, classes, labels); + drawPointLabels(selection, labels, filter, classes); - function hasText(d, i) { - return labels[i].hasOwnProperty('x') && labels[i].hasOwnProperty('y'); + function hasText(d) { + return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y'); } } - function drawAreaIcons(selection, entities, filter, classes, labels) { + function drawAreaIcons(selection, labels, filter, classes) { var icons = selection.selectAll('use.' + classes) - .filter(filter) - .data(entities, osmEntity.key); + .filter(d => filter(d.entity)) + .data(labels, d => osmEntity.key(d.entity)); // exit icons.exit() @@ -201,9 +172,9 @@ export function svgLabels(projection, context) { .attr('width', '17px') .attr('height', '17px') .merge(icons) - .attr('transform', get(labels, 'transform')) + .attr('transform', d => d.position.transform) .attr('xlink:href', function(d) { - var preset = presetManager.match(d, context.graph()); + var preset = presetManager.match(d.entity, context.graph()); var picon = preset && preset.icon; return picon ? '#' + picon : ''; }); @@ -280,27 +251,34 @@ export function svgLabels(projection, context) { // Insert collision boxes around interesting points/vertices if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) { + const isAddr = isAddressPoint(entity.tags); var hasDirections = entity.directions(graph, projection).length; - var markerPadding; + var markerPadding = 0; - if (!wireframe && geometry === 'point' && !(zoom >= 18 && hasDirections)) { - renderNodeAs[entity.id] = 'point'; - markerPadding = 20; // extra y for marker height + if (wireframe) { + renderNodeAs[entity.id] = { geometry: 'vertex', isAddr }; + } else if (geometry === 'vertex') { + renderNodeAs[entity.id] = { geometry: 'vertex', isAddr }; + } else if (zoom >= 18 && hasDirections) { + renderNodeAs[entity.id] = { geometry: 'vertex', isAddr }; } else { - renderNodeAs[entity.id] = 'vertex'; - markerPadding = 0; + renderNodeAs[entity.id] = { geometry: 'point', isAddr }; + markerPadding = 20; // extra y for marker height } - var coord = projection(entity.loc); - var nodePadding = 10; - var bbox = { - minX: coord[0] - nodePadding, - minY: coord[1] - nodePadding - markerPadding, - maxX: coord[0] + nodePadding, - maxY: coord[1] + nodePadding - }; - - doInsert(bbox, entity.id + 'P'); + if (isAddr) { + undoInsert(entity.id + 'P'); + } else { + var coord = projection(entity.loc); + var nodePadding = 10; + var bbox = { + minX: coord[0] - nodePadding, + minY: coord[1] - nodePadding - markerPadding, + maxX: coord[0] + nodePadding, + maxY: coord[1] + nodePadding + }; + doInsert(bbox, entity.id + 'P'); + } } // From here on, treat vertices like points @@ -327,12 +305,6 @@ export function svgLabels(projection, context) { } } - var positions = { - point: [], - line: [], - area: [] - }; - var labelled = { point: [], line: [], @@ -349,7 +321,7 @@ export function svgLabels(projection, context) { var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName; var name = getName(entity); - var width = name && textWidth(name, fontSize); + var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node()); var p = null; if (geometry === 'point' || geometry === 'vertex') { @@ -357,10 +329,13 @@ export function svgLabels(projection, context) { // no vertex labels at low zooms (vertices have no icons) if (wireframe) continue; var renderAs = renderNodeAs[entity.id]; - if (renderAs === 'vertex' && zoom < 17) continue; + if (renderAs.geometry === 'vertex' && zoom < 17) continue; + while (renderAs.isAddr && width > 36) { + name = `${name.substring(0, name.replace(/…$/, '').length - 1)}…`; + width = textWidth(name, fontSize, selection.select('g.layer-osm.labels').node()); + } p = getPointLabel(entity, width, fontSize, renderAs); - } else if (geometry === 'line') { p = getLineLabel(entity, width, fontSize); @@ -371,8 +346,11 @@ export function svgLabels(projection, context) { if (p) { if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point p.classes = geometry + ' tag-' + labelStack[k][1]; - positions[geometry].push(p); - labelled[geometry].push(entity); + labelled[geometry].push({ + entity, + name, + position: p + }); } } } @@ -391,29 +369,39 @@ export function svgLabels(projection, context) { } - function getPointLabel(entity, width, height, geometry) { - var y = (geometry === 'point' ? -12 : 0); + function getPointLabel(entity, width, height, style) { + var y = (style.geometry === 'point' ? -12 : 0); var pointOffsets = { ltr: [15, y, 'start'], rtl: [-15, y, 'end'] }; + const isAddr = style.isAddr; var textDirection = localizer.textDirection(); var coord = projection(entity.loc); var textPadding = 2; var offset = pointOffsets[textDirection]; + if (isAddr) offset = [0, 1, 'middle']; var p = { height: height, width: width, x: coord[0] + offset[0], y: coord[1] + offset[1], - textAnchor: offset[2] + textAnchor: offset[2], + isAddr }; // insert a collision box for the text label.. - var bbox; - if (textDirection === 'rtl') { + let bbox; + if (isAddr) { + bbox = { + minX: p.x - (width / 2) - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + (width / 2) + textPadding, + maxY: p.y + (height / 2) + textPadding + }; + } else if (textDirection === 'rtl') { bbox = { minX: p.x - width - textPadding, minY: p.y - (height / 2) - textPadding, @@ -559,7 +547,7 @@ export function svgLabels(projection, context) { var padding = 2; var p = {}; - if (picon) { // icon and label.. + if (picon && !shouldSkipIcon(preset)) { // icon and label.. if (addIcon()) { addLabel(iconSize + padding); return p; @@ -625,6 +613,13 @@ export function svgLabels(projection, context) { _rdrawn.insert(bbox); } + function undoInsert(id) { + var oldbox = _entitybboxes[id]; + if (oldbox) { + _rdrawn.remove(oldbox); + } + delete _entitybboxes[id]; + } function tryInsert(bboxes, id, saveSkipped) { var skipped = false; @@ -670,19 +665,19 @@ export function svgLabels(projection, context) { var debug = layer.selectAll('.labels-group.debug'); // points - drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); - drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point); + drawPointLabels(label, labelled.point, filter, 'pointlabel'); + drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo'); // lines - drawLinePaths(layer, labelled.line, filter, '', positions.line); - drawLineLabels(label, labelled.line, filter, 'linelabel', positions.line); - drawLineLabels(halo, labelled.line, filter, 'linelabel-halo', positions.line); + drawLinePaths(layer, labelled.line, filter, ''); + drawLineLabels(label, labelled.line, filter, 'linelabel'); + drawLineLabels(halo, labelled.line, filter, 'linelabel-halo'); // areas - drawAreaLabels(label, labelled.area, filter, 'arealabel', positions.area); - drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo', positions.area); - drawAreaIcons(label, labelled.area, filter, 'areaicon', positions.area); - drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); + drawAreaLabels(label, labelled.area, filter, 'arealabel'); + drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo'); + drawAreaIcons(label, labelled.area, filter, 'areaicon'); + drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo'); // debug drawCollisionBoxes(debug, _rskipped, 'debug-skipped'); @@ -700,26 +695,17 @@ export function svgLabels(projection, context) { .classed('nolabel', false); var mouse = context.map().mouse(); - var graph = context.graph(); - var selectedIDs = context.selectedIDs(); var ids = []; var pad, bbox; // hide labels near the mouse - if (mouse) { + if (mouse && context.mode().id !== 'browse' && context.mode().id !== 'select') { pad = 20; bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad }; var nearMouse = _rdrawn.search(bbox).map(function(entity) { return entity.id; }); ids.push.apply(ids, nearMouse); } - - // hide labels on selected nodes (they look weird when dragging / haloed) - for (var i = 0; i < selectedIDs.length; i++) { - var entity = graph.hasEntity(selectedIDs[i]); - if (entity && entity.type === 'node') { - ids.push(selectedIDs[i]); - } - } + ids = utilArrayDifference(ids, context.mode()?.selectedIDs?.() || []); layers.selectAll(utilEntitySelector(ids)) .classed('nolabel', true); @@ -776,3 +762,41 @@ export function svgLabels(projection, context) { return drawLabels; } + + +const _textWidthCache = {}; +export function textWidth(text, size, container) { + let c = _textWidthCache[size]; + if (!c) c = _textWidthCache[size] = {}; + + if (c[text]) { + return c[text]; + } + const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + elem.style.fontSize = `${size}px`; + elem.textContent = text; + container.appendChild(elem); + c[text] = elem.getComputedTextLength(); + elem.remove(); + return c[text]; +} + + +const nonPrimaryKeys = new Set([ + 'check_date', + 'fixme', + 'layer', + 'level', + 'level:ref', + 'note' +]); +const nonPrimaryKeysRegex = /^(ref|survey|note):/; +export function isAddressPoint(tags) { + const keys = Object.keys(tags); + return keys.length > 0 && keys.every(key => + key.startsWith('addr:') || + !osmIsInterestingTag(key) || + nonPrimaryKeys.has(key) || + nonPrimaryKeysRegex.test(key) + ); +} diff --git a/modules/svg/points.js b/modules/svg/points.js index 03ba5b738..618c6c453 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,29 +1,44 @@ import deepEqual from 'fast-deep-equal'; +import { clamp } from 'lodash-es'; + import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './helpers'; import { svgTagClasses } from './tag_classes'; import { presetManager } from '../presets'; +import { textWidth, isAddressPoint } from './labels'; export function svgPoints(projection, context) { function markerPath(selection, klass) { selection .attr('class', klass) - .attr('transform', 'translate(-8, -23)') - .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z'); + .attr('transform', d => isAddressPoint(d.tags) + ? `translate(-${addressShieldWidth(d, selection)/2}, -8)` + : 'translate(-8, -23)') + .attr('d', d => { + if (!isAddressPoint(d.tags)) { + return 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z'; + } + const shieldWidth = addressShieldWidth(d, selection); + return `M ${shieldWidth},8 C ${shieldWidth},15 ${shieldWidth-2},16 ${shieldWidth-8},16 L 8,16 C 2,16 0,15 0,8 C 0,2 2,0 8,0 L ${shieldWidth-8},0 C ${shieldWidth-2},0 ${shieldWidth},2 ${shieldWidth},8 z`; + }); } function sortY(a, b) { return b.loc[1] - a.loc[1]; } + function addressShieldWidth(d, selection) { + const width = textWidth(d.tags['addr:housenumber'] || d.tags['addr:housename'] || '', 10, selection.node().parentElement); + return clamp(width, 10, 34) + 8; + }; // Avoid exit/enter if we're just moving stuff around. // The node will get a new version but we only need to run the update selection. function fastEntityKey(d) { - var mode = context.mode(); - var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + const mode = context.mode(); + const isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); return isMoving ? d.id : osmEntity.key(d); } @@ -42,15 +57,16 @@ export function svgPoints(projection, context) { id: node.id, properties: { target: true, - entity: node + entity: node, + isAddr: isAddressPoint(node.tags) }, geometry: node.asGeoJSON() }); }); var targets = selection.selectAll('.point.target') - .filter(function(d) { return filter(d.properties.entity); }) - .data(data, function key(d) { return d.id; }); + .filter(d => filter(d.properties.entity)) + .data(data, d => fastEntityKey(d.properties.entity)); // exit targets.exit() @@ -59,12 +75,12 @@ export function svgPoints(projection, context) { // enter/update targets.enter() .append('rect') - .attr('x', -10) - .attr('y', -26) - .attr('width', 20) - .attr('height', 30) - .merge(targets) + .attr('x', d => d.properties.isAddr ? -addressShieldWidth(d.properties.entity, selection) / 2 : -10) + .attr('y', d => d.properties.isAddr ? -8 : -26) + .attr('width', d => d.properties.isAddr ? addressShieldWidth(d.properties.entity, selection) : 20) + .attr('height', d => d.properties.isAddr ? 16 : 30) .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) + .merge(targets) .attr('transform', getTransform); } diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index ba2956ab3..f47c2de22 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -202,8 +202,8 @@ export function uiEntityEditor(context) { context.overwrite(combinedAction, annotation); } else { context.perform(combinedAction, annotation); - _coalesceChanges = !!onInput; } + _coalesceChanges = !!onInput; } // if leaving field (blur event), rerun validation @@ -259,7 +259,6 @@ export function uiEntityEditor(context) { context.overwrite(combinedAction, annotation); } else { context.perform(combinedAction, annotation); - _coalesceChanges = false; } } diff --git a/modules/ui/fields/address.js b/modules/ui/fields/address.js index f706a5f3f..7743ca1b4 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -339,12 +339,10 @@ export function uiFieldAddress(field, context) { } _wrap.selectAll('input') + .on('input', change(true)) .on('blur', change()) .on('change', change()); - _wrap.selectAll('input:not(.combobox-input)') - .on('input', change(true)); - if (_tags) updateTags(_tags); } diff --git a/modules/ui/photoviewer.js b/modules/ui/photoviewer.js index a58674899..f81abaef7 100644 --- a/modules/ui/photoviewer.js +++ b/modules/ui/photoviewer.js @@ -1,6 +1,7 @@ import { select as d3_select } from 'd3-selection'; +import { clamp } from 'lodash-es'; import { t } from '../core/localizer'; import { dispatch as d3_dispatch } from 'd3-dispatch'; @@ -230,10 +231,6 @@ export function uiPhotoviewer(context) { dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target)); } - function clamp(num, min, max) { - return Math.max(min, Math.min(num, max)); - } - function stopResize(d3_event) { if (pointerId !== (d3_event.pointerId || 'mouse')) return; diff --git a/modules/ui/sections/background_display_options.js b/modules/ui/sections/background_display_options.js index 19bd24d7c..8b98a7cc7 100644 --- a/modules/ui/sections/background_display_options.js +++ b/modules/ui/sections/background_display_options.js @@ -1,6 +1,7 @@ import { select as d3_select } from 'd3-selection'; +import { clamp } from 'lodash-es'; import { prefs } from '../../core/preferences'; import { t, localizer } from '../../core/localizer'; @@ -27,10 +28,6 @@ export function uiSectionBackgroundDisplayOptions(context) { sharpness: 1 }; - function clamp(x, min, max) { - return Math.max(min, Math.min(x, max)); - } - function updateValue(d, val) { val = clamp(val, _minVal, _maxVal); diff --git a/modules/util/tiler.js b/modules/util/tiler.js index ead3a75b7..6b1410098 100644 --- a/modules/util/tiler.js +++ b/modules/util/tiler.js @@ -1,4 +1,6 @@ import { range as d3_range } from 'd3-array'; +import { clamp } from 'lodash-es'; + import { geoExtent, geoScaleToZoom } from '../geo'; @@ -12,11 +14,6 @@ export function utilTiler() { var _skipNullIsland = false; - function clamp(num, min, max) { - return Math.max(min, Math.min(num, max)); - } - - function nearNullIsland(tile) { var x = tile[0]; var y = tile[1]; diff --git a/modules/util/units.js b/modules/util/units.js index fb745830a..0483b1a90 100644 --- a/modules/util/units.js +++ b/modules/util/units.js @@ -1,3 +1,5 @@ +import { clamp } from 'lodash-es'; + import { t, localizer } from '../core/localizer'; var OSM_PRECISION = 7; @@ -102,10 +104,6 @@ function wrap(x, min, max) { return ((x - min) % d + d) % d + min; } -function clamp(x, min, max) { - return Math.max(min, Math.min(x, max)); -} - function roundToDecimal (target, decimalPlace) { target = Number(target); decimalPlace = Number(decimalPlace); diff --git a/modules/util/util.js b/modules/util/util.js index 58a223385..b7b928019 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -191,6 +191,7 @@ export function utilDisplayName(entity, hideNetwork) { var name = entity.tags[localizedNameKey] || entity.tags.name || ''; var tags = { + addr: entity.tags['addr:housenumber'] || entity.tags['addr:housename'], direction: entity.tags.direction, from: entity.tags.from, name, @@ -210,6 +211,10 @@ export function utilDisplayName(entity, hideNetwork) { if (!entity.tags.route && name) { return name; } + // unnamed buildings or address nodes: show housenumber/housename + if (tags.addr) { + return tags.addr; + } var keyComponents = []; diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index f4085f941..3f2e27123 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -1,4 +1,4 @@ -import { isEqual } from 'lodash'; +import { isEqual } from 'lodash-es'; import { actionAddMidpoint } from '../actions/add_midpoint'; import { actionChangeTags } from '../actions/change_tags'; @@ -447,8 +447,8 @@ export function validationCrossingWays(context) { var entity1 = graph.hasEntity(this.entityIds[0]), entity2 = graph.hasEntity(this.entityIds[1]); return (entity1 && entity2) ? t.append('issues.crossing_ways.message', { - feature: utilDisplayLabel(entity1, graph), - feature2: utilDisplayLabel(entity2, graph) + feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'), + feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building') }) : ''; }, reference: showReference,