render addresses (housenumber/housename)

* points with a dedicated marker
* text inside of areas
This commit is contained in:
Martin Raifer
2025-04-04 18:30:18 +02:00
parent 552c6b6148
commit 4254e67ca7
10 changed files with 99 additions and 48 deletions

View File

@@ -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."

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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});
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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 = [];

View File

@@ -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,