From 4254e67ca7e10349798ded6d35a36d197f26a060 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 4 Apr 2025 18:30:18 +0200 Subject: [PATCH] render addresses (housenumber/housename) * points with a dedicated marker * text inside of areas --- data/core.yaml | 3 ++ modules/core/history.js | 2 +- modules/renderer/features.js | 13 ++++- modules/renderer/map.js | 4 +- modules/svg/labels.js | 81 +++++++++++++++++----------- modules/svg/points.js | 28 ++++++++-- modules/ui/entity_editor.js | 3 +- modules/ui/fields/address.js | 4 +- modules/util/util.js | 5 ++ modules/validations/crossing_ways.js | 4 +- 10 files changed, 99 insertions(+), 48 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 2a8ef60d7..679aaa118 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/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/renderer/features.js b/modules/renderer/features.js index 0263f3ad2..eeae822db 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'; @@ -103,9 +103,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 b5d71a369..6b940bfb2 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -393,8 +393,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/svg/labels.js b/modules/svg/labels.js index fa0bcabd8..d9ffd5d4b 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'; @@ -27,7 +27,7 @@ export function svgLabels(projection, context) { 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 +62,9 @@ export function svgLabels(projection, context) { ['point', 'ref', '*', 10], ['line', 'name', '*', 12], ['area', 'name', '*', 12], - ['point', 'name', '*', 10] + ['point', 'name', '*', 10], + ['point', 'addr:housenumber', '*', 8], + ['point', 'addr:housename', '*', 8] ]; @@ -163,14 +165,14 @@ export function svgLabels(projection, context) { .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) - .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); - }); + }) + .merge(texts) + .attr('x', get(labels, 'x')) + .attr('y', get(labels, 'y')); } @@ -291,16 +293,20 @@ export function svgLabels(projection, context) { markerPadding = 0; } - 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 - }; + if (!isAddressPoint(entity.tags)) { + 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'); + doInsert(bbox, entity.id + 'P'); + } else { + undoInsert(entity.id + 'P'); + } } // From here on, treat vertices like points @@ -391,18 +397,26 @@ export function svgLabels(projection, context) { } + function isAddressPoint(tags) { + const keys = Object.keys(tags); + return keys.length > 0 && keys.every(key => + key.startsWith('addr:') || !osmIsInterestingTag(key)); + } + function getPointLabel(entity, width, height, geometry) { var y = (geometry === 'point' ? -12 : 0); var pointOffsets = { ltr: [15, y, 'start'], rtl: [-15, y, 'end'] }; + const isAddr = isAddressPoint(entity.tags); 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, @@ -412,8 +426,15 @@ export function svgLabels(projection, context) { }; // 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 +580,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 +646,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; @@ -700,26 +728,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); diff --git a/modules/svg/points.js b/modules/svg/points.js index 03ba5b738..1e4056a86 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,6 +1,6 @@ import deepEqual from 'fast-deep-equal'; import { geoScaleToZoom } from '../geo'; -import { osmEntity } from '../osm'; +import { osmEntity, osmIsInterestingTag } from '../osm'; import { svgPointTransform } from './helpers'; import { svgTagClasses } from './tag_classes'; import { presetManager } from '../presets'; @@ -8,10 +8,28 @@ import { presetManager } from '../presets'; export function svgPoints(projection, context) { function markerPath(selection, klass) { + const isHousenumber = d => { + const tagKeys = Object.keys(d.tags); + if (tagKeys.length === 0) return false; + //return d.tags['addr:housenumber'] && + return Object.keys(d.tags).every(key => + key.startsWith('addr:') || !osmIsInterestingTag(key)); + }; + const addressShieldWidth = d => { + return Math.min(6, Math.max(2, (d.tags['addr:housenumber'] || d.tags['addr:housename'] || '').length)) * 6 + 6; + }; 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 => isHousenumber(d) + ? `translate(-${addressShieldWidth(d)/2}, -8)` + : 'translate(-8, -23)') + .attr('d', d => { + if (!isHousenumber(d)) { + 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); + 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) { @@ -22,8 +40,8 @@ export function svgPoints(projection, context) { // 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); } 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 2b7851215..2add9d713 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -338,12 +338,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/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..4158d24da 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -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,