mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
804 lines
27 KiB
JavaScript
804 lines
27 KiB
JavaScript
import _throttle from 'lodash-es/throttle';
|
|
|
|
import { geoPath as d3_geoPath } from 'd3-geo';
|
|
import RBush from 'rbush';
|
|
import { localizer } from '../core/localizer';
|
|
|
|
import {
|
|
geoExtent, geoPolygonIntersectsPolygon, geoPathLength,
|
|
geoScaleToZoom, geoVecInterp, geoVecLength
|
|
} from '../geo';
|
|
import { presetManager } from '../presets';
|
|
import { osmEntity, osmIsInterestingTag } from '../osm';
|
|
import { utilDetect } from '../util/detect';
|
|
import { utilArrayDifference, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util';
|
|
|
|
|
|
|
|
export function svgLabels(projection, context) {
|
|
var path = d3_geoPath(projection);
|
|
var detected = utilDetect();
|
|
var baselineHack = (detected.ie ||
|
|
detected.browser.toLowerCase() === 'edge' ||
|
|
(detected.browser.toLowerCase() === 'firefox' && detected.version >= 70));
|
|
var _rdrawn = new RBush();
|
|
var _rskipped = new RBush();
|
|
var _entitybboxes = {};
|
|
|
|
// Listed from highest to lowest priority
|
|
const labelStack = [
|
|
['line', 'aeroway', '*', 12],
|
|
['line', 'highway', 'motorway', 12],
|
|
['line', 'highway', 'trunk', 12],
|
|
['line', 'highway', 'primary', 12],
|
|
['line', 'highway', 'secondary', 12],
|
|
['line', 'highway', 'tertiary', 12],
|
|
['line', 'highway', '*', 12],
|
|
['line', 'railway', '*', 12],
|
|
['line', 'waterway', '*', 12],
|
|
['area', 'aeroway', '*', 12],
|
|
['area', 'amenity', '*', 12],
|
|
['area', 'building', '*', 12],
|
|
['area', 'historic', '*', 12],
|
|
['area', 'leisure', '*', 12],
|
|
['area', 'man_made', '*', 12],
|
|
['area', 'natural', '*', 12],
|
|
['area', 'shop', '*', 12],
|
|
['area', 'tourism', '*', 12],
|
|
['area', 'camp_site', '*', 12],
|
|
['point', 'aeroway', '*', 10],
|
|
['point', 'amenity', '*', 10],
|
|
['point', 'building', '*', 10],
|
|
['point', 'historic', '*', 10],
|
|
['point', 'leisure', '*', 10],
|
|
['point', 'man_made', '*', 10],
|
|
['point', 'natural', '*', 10],
|
|
['point', 'shop', '*', 10],
|
|
['point', 'tourism', '*', 10],
|
|
['point', 'camp_site', '*', 10],
|
|
['line', 'ref', '*', 12],
|
|
['area', 'ref', '*', 12],
|
|
['point', 'ref', '*', 10],
|
|
['line', 'name', '*', 12],
|
|
['area', 'name', '*', 12],
|
|
['point', 'name', '*', 10],
|
|
['point', 'addr:housenumber', '*', 10],
|
|
['point', 'addr:housename', '*', 10]
|
|
];
|
|
|
|
|
|
function shouldSkipIcon(preset) {
|
|
var noIcons = ['building', 'landuse', 'natural'];
|
|
return noIcons.some(function(s) {
|
|
return preset.id.indexOf(s) >= 0;
|
|
});
|
|
}
|
|
|
|
|
|
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()
|
|
.remove();
|
|
|
|
// enter/update
|
|
paths.enter()
|
|
.append('path')
|
|
.style('stroke-width', d => d.position['font-size'])
|
|
.attr('id', d => 'ideditor-labelpath-' + d.entity.id)
|
|
.attr('class', classes)
|
|
.merge(paths)
|
|
.attr('d', d => d.position.lineString);
|
|
}
|
|
|
|
|
|
function drawLineLabels(selection, labels, filter, classes) {
|
|
var texts = selection.selectAll('text.' + classes)
|
|
.filter(d => filter(d.entity))
|
|
.data(labels, d => osmEntity.key(d.entity));
|
|
|
|
// exit
|
|
texts.exit()
|
|
.remove();
|
|
|
|
// enter
|
|
texts.enter()
|
|
.append('text')
|
|
.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(d => filter(d.entity))
|
|
.data(labels, d => osmEntity.key(d.entity))
|
|
.attr('startOffset', '50%')
|
|
.attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; })
|
|
.text(d => d.name);
|
|
}
|
|
|
|
|
|
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(d => filter(d.entity))
|
|
.data(labels, d => osmEntity.key(d.entity));
|
|
|
|
// exit
|
|
texts.exit()
|
|
.remove();
|
|
|
|
// enter/update
|
|
texts.enter()
|
|
.append('text')
|
|
.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', d => d.position.x)
|
|
.attr('y', d => d.position.y);
|
|
}
|
|
|
|
|
|
function drawAreaLabels(selection, labels, filter, classes) {
|
|
labels = labels.filter(hasText);
|
|
drawPointLabels(selection, labels, filter, classes);
|
|
|
|
function hasText(d) {
|
|
return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y');
|
|
}
|
|
}
|
|
|
|
|
|
function drawAreaIcons(selection, labels, filter, classes) {
|
|
var icons = selection.selectAll('use.' + classes)
|
|
.filter(d => filter(d.entity))
|
|
.data(labels, d => osmEntity.key(d.entity));
|
|
|
|
// exit
|
|
icons.exit()
|
|
.remove();
|
|
|
|
// enter/update
|
|
icons.enter()
|
|
.append('use')
|
|
.attr('class', 'icon ' + classes)
|
|
.attr('width', '17px')
|
|
.attr('height', '17px')
|
|
.merge(icons)
|
|
.attr('transform', d => d.position.transform)
|
|
.attr('xlink:href', function(d) {
|
|
var preset = presetManager.match(d.entity, context.graph());
|
|
var picon = preset && preset.icon;
|
|
return picon ? '#' + picon : '';
|
|
});
|
|
}
|
|
|
|
|
|
function drawCollisionBoxes(selection, rtree, which) {
|
|
var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');
|
|
|
|
var gj = [];
|
|
if (context.getDebug('collision')) {
|
|
gj = rtree.all().map(function(d) {
|
|
return { type: 'Polygon', coordinates: [[
|
|
[d.minX, d.minY],
|
|
[d.maxX, d.minY],
|
|
[d.maxX, d.maxY],
|
|
[d.minX, d.maxY],
|
|
[d.minX, d.minY]
|
|
]]};
|
|
});
|
|
}
|
|
|
|
var boxes = selection.selectAll('.' + which)
|
|
.data(gj);
|
|
|
|
// exit
|
|
boxes.exit()
|
|
.remove();
|
|
|
|
// enter/update
|
|
boxes.enter()
|
|
.append('path')
|
|
.attr('class', classes)
|
|
.merge(boxes)
|
|
.attr('d', d3_geoPath());
|
|
}
|
|
|
|
|
|
function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {
|
|
var wireframe = context.surface().classed('fill-wireframe');
|
|
var zoom = geoScaleToZoom(projection.scale());
|
|
|
|
var labelable = [];
|
|
var renderNodeAs = {};
|
|
var i, j, k, entity, geometry;
|
|
|
|
for (i = 0; i < labelStack.length; i++) {
|
|
labelable.push([]);
|
|
}
|
|
|
|
if (fullRedraw) {
|
|
_rdrawn.clear();
|
|
_rskipped.clear();
|
|
_entitybboxes = {};
|
|
|
|
} else {
|
|
for (i = 0; i < entities.length; i++) {
|
|
entity = entities[i];
|
|
var toRemove = []
|
|
.concat(_entitybboxes[entity.id] || [])
|
|
.concat(_entitybboxes[entity.id + 'I'] || []);
|
|
|
|
for (j = 0; j < toRemove.length; j++) {
|
|
_rdrawn.remove(toRemove[j]);
|
|
_rskipped.remove(toRemove[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop through all the entities to do some preprocessing
|
|
for (i = 0; i < entities.length; i++) {
|
|
entity = entities[i];
|
|
geometry = entity.geometry(graph);
|
|
|
|
// 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 = 0;
|
|
|
|
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] = { geometry: 'point', isAddr };
|
|
markerPadding = 20; // extra y for marker height
|
|
}
|
|
|
|
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
|
|
if (geometry === 'vertex') {
|
|
geometry = 'point';
|
|
}
|
|
|
|
// Determine which entities are label-able
|
|
var preset = geometry === 'area' && presetManager.match(entity, graph);
|
|
var icon = preset && !shouldSkipIcon(preset) && preset.icon;
|
|
|
|
if (!icon && !utilDisplayName(entity)) continue;
|
|
|
|
for (k = 0; k < labelStack.length; k++) {
|
|
var matchGeom = labelStack[k][0];
|
|
var matchKey = labelStack[k][1];
|
|
var matchVal = labelStack[k][2];
|
|
var hasVal = entity.tags[matchKey];
|
|
|
|
if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) {
|
|
labelable[k].push(entity);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var labelled = {
|
|
point: [],
|
|
line: [],
|
|
area: []
|
|
};
|
|
|
|
// Try and find a valid label for labellable entities
|
|
for (k = 0; k < labelable.length; k++) {
|
|
var fontSize = labelStack[k][3];
|
|
|
|
for (i = 0; i < labelable[k].length; i++) {
|
|
entity = labelable[k][i];
|
|
geometry = entity.geometry(graph);
|
|
|
|
var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName;
|
|
var name = getName(entity);
|
|
var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());
|
|
var p = null;
|
|
|
|
if (geometry === 'point' || geometry === 'vertex') {
|
|
// no point or vertex labels in wireframe mode
|
|
// no vertex labels at low zooms (vertices have no icons)
|
|
if (wireframe) continue;
|
|
var renderAs = renderNodeAs[entity.id];
|
|
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);
|
|
|
|
} else if (geometry === 'area') {
|
|
p = getAreaLabel(entity, width, fontSize);
|
|
}
|
|
|
|
if (p) {
|
|
if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point
|
|
p.classes = geometry + ' tag-' + labelStack[k][1];
|
|
labelled[geometry].push({
|
|
entity,
|
|
name,
|
|
position: p
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function isInterestingVertex(entity) {
|
|
var selectedIDs = context.selectedIDs();
|
|
|
|
return entity.hasInterestingTags() ||
|
|
entity.isEndpoint(graph) ||
|
|
entity.isConnected(graph) ||
|
|
selectedIDs.indexOf(entity.id) !== -1 ||
|
|
graph.parentWays(entity).some(function(parent) {
|
|
return selectedIDs.indexOf(parent.id) !== -1;
|
|
});
|
|
}
|
|
|
|
|
|
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],
|
|
isAddr
|
|
};
|
|
|
|
// insert a collision box for the text label..
|
|
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,
|
|
maxX: p.x + textPadding,
|
|
maxY: p.y + (height / 2) + textPadding
|
|
};
|
|
} else {
|
|
bbox = {
|
|
minX: p.x - textPadding,
|
|
minY: p.y - (height / 2) - textPadding,
|
|
maxX: p.x + width + textPadding,
|
|
maxY: p.y + (height / 2) + textPadding
|
|
};
|
|
}
|
|
|
|
if (tryInsert([bbox], entity.id, true)) {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
|
|
function getLineLabel(entity, width, height) {
|
|
var viewport = geoExtent(context.projection.clipExtent()).polygon();
|
|
var points = graph.childNodes(entity)
|
|
.map(function(node) { return projection(node.loc); });
|
|
var length = geoPathLength(points);
|
|
|
|
if (length < width + 20) return;
|
|
|
|
// % along the line to attempt to place the label
|
|
var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,
|
|
25, 75, 20, 80, 15, 95, 10, 90, 5, 95];
|
|
var padding = 3;
|
|
|
|
for (var i = 0; i < lineOffsets.length; i++) {
|
|
var offset = lineOffsets[i];
|
|
var middle = offset / 100 * length;
|
|
var start = middle - width / 2;
|
|
|
|
if (start < 0 || start + width > length) continue;
|
|
|
|
// generate subpath and ignore paths that are invalid or don't cross viewport.
|
|
var sub = subpath(points, start, start + width);
|
|
if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {
|
|
continue;
|
|
}
|
|
|
|
var isReverse = reverse(sub);
|
|
if (isReverse) {
|
|
sub = sub.reverse();
|
|
}
|
|
|
|
var bboxes = [];
|
|
var boxsize = (height + 2) / 2;
|
|
|
|
for (var j = 0; j < sub.length - 1; j++) {
|
|
var a = sub[j];
|
|
var b = sub[j + 1];
|
|
|
|
// split up the text into small collision boxes
|
|
var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));
|
|
|
|
for (var box = 0; box < num; box++) {
|
|
var p = geoVecInterp(a, b, box / num);
|
|
var x0 = p[0] - boxsize - padding;
|
|
var y0 = p[1] - boxsize - padding;
|
|
var x1 = p[0] + boxsize + padding;
|
|
var y1 = p[1] + boxsize + padding;
|
|
|
|
bboxes.push({
|
|
minX: Math.min(x0, x1),
|
|
minY: Math.min(y0, y1),
|
|
maxX: Math.max(x0, x1),
|
|
maxY: Math.max(y0, y1)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (tryInsert(bboxes, entity.id, false)) { // accept this one
|
|
return {
|
|
'font-size': height + 2,
|
|
lineString: lineString(sub),
|
|
startOffset: offset + '%'
|
|
};
|
|
}
|
|
}
|
|
|
|
function reverse(p) {
|
|
var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);
|
|
return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);
|
|
}
|
|
|
|
function lineString(points) {
|
|
return 'M' + points.join('L');
|
|
}
|
|
|
|
function subpath(points, from, to) {
|
|
var sofar = 0;
|
|
var start, end, i0, i1;
|
|
|
|
for (var i = 0; i < points.length - 1; i++) {
|
|
var a = points[i];
|
|
var b = points[i + 1];
|
|
var current = geoVecLength(a, b);
|
|
var portion;
|
|
if (!start && sofar + current >= from) {
|
|
portion = (from - sofar) / current;
|
|
start = [
|
|
a[0] + portion * (b[0] - a[0]),
|
|
a[1] + portion * (b[1] - a[1])
|
|
];
|
|
i0 = i + 1;
|
|
}
|
|
if (!end && sofar + current >= to) {
|
|
portion = (to - sofar) / current;
|
|
end = [
|
|
a[0] + portion * (b[0] - a[0]),
|
|
a[1] + portion * (b[1] - a[1])
|
|
];
|
|
i1 = i + 1;
|
|
}
|
|
sofar += current;
|
|
}
|
|
|
|
var result = points.slice(i0, i1);
|
|
result.unshift(start);
|
|
result.push(end);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
function getAreaLabel(entity, width, height) {
|
|
var centroid = path.centroid(entity.asGeoJSON(graph));
|
|
var extent = entity.extent(graph);
|
|
var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];
|
|
|
|
if (isNaN(centroid[0]) || areaWidth < 20) return;
|
|
|
|
var preset = presetManager.match(entity, context.graph());
|
|
var picon = preset && preset.icon;
|
|
var iconSize = 17;
|
|
var padding = 2;
|
|
var p = {};
|
|
|
|
if (picon && !shouldSkipIcon(preset)) { // icon and label..
|
|
if (addIcon()) {
|
|
addLabel(iconSize + padding);
|
|
return p;
|
|
}
|
|
} else { // label only..
|
|
if (addLabel(0)) {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
|
|
function addIcon() {
|
|
var iconX = centroid[0] - (iconSize / 2);
|
|
var iconY = centroid[1] - (iconSize / 2);
|
|
var bbox = {
|
|
minX: iconX,
|
|
minY: iconY,
|
|
maxX: iconX + iconSize,
|
|
maxY: iconY + iconSize
|
|
};
|
|
|
|
if (tryInsert([bbox], entity.id + 'I', true)) {
|
|
p.transform = 'translate(' + iconX + ',' + iconY + ')';
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function addLabel(yOffset) {
|
|
if (width && areaWidth >= width + 20) {
|
|
var labelX = centroid[0];
|
|
var labelY = centroid[1] + yOffset;
|
|
var bbox = {
|
|
minX: labelX - (width / 2) - padding,
|
|
minY: labelY - (height / 2) - padding,
|
|
maxX: labelX + (width / 2) + padding,
|
|
maxY: labelY + (height / 2) + padding
|
|
};
|
|
|
|
if (tryInsert([bbox], entity.id, true)) {
|
|
p.x = labelX;
|
|
p.y = labelY;
|
|
p.textAnchor = 'middle';
|
|
p.height = height;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// force insert a singular bounding box
|
|
// singular box only, no array, id better be unique
|
|
function doInsert(bbox, id) {
|
|
bbox.id = id;
|
|
|
|
var oldbox = _entitybboxes[id];
|
|
if (oldbox) {
|
|
_rdrawn.remove(oldbox);
|
|
}
|
|
_entitybboxes[id] = bbox;
|
|
_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;
|
|
|
|
for (var i = 0; i < bboxes.length; i++) {
|
|
var bbox = bboxes[i];
|
|
bbox.id = id;
|
|
|
|
// Check that label is visible
|
|
if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {
|
|
skipped = true;
|
|
break;
|
|
}
|
|
if (_rdrawn.collides(bbox)) {
|
|
skipped = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
_entitybboxes[id] = bboxes;
|
|
|
|
if (skipped) {
|
|
if (saveSkipped) {
|
|
_rskipped.load(bboxes);
|
|
}
|
|
} else {
|
|
_rdrawn.load(bboxes);
|
|
}
|
|
|
|
return !skipped;
|
|
}
|
|
|
|
|
|
var layer = selection.selectAll('.layer-osm.labels');
|
|
layer.selectAll('.labels-group')
|
|
.data(['halo', 'label', 'debug'])
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'labels-group ' + d; });
|
|
|
|
var halo = layer.selectAll('.labels-group.halo');
|
|
var label = layer.selectAll('.labels-group.label');
|
|
var debug = layer.selectAll('.labels-group.debug');
|
|
|
|
// points
|
|
drawPointLabels(label, labelled.point, filter, 'pointlabel');
|
|
drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo');
|
|
|
|
// lines
|
|
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');
|
|
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');
|
|
drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');
|
|
|
|
layer.call(filterLabels);
|
|
}
|
|
|
|
|
|
function filterLabels(selection) {
|
|
var drawLayer = selection.selectAll('.layer-osm.labels');
|
|
var layers = drawLayer.selectAll('.labels-group.halo, .labels-group.label');
|
|
|
|
layers.selectAll('.nolabel')
|
|
.classed('nolabel', false);
|
|
|
|
var mouse = context.map().mouse();
|
|
var ids = [];
|
|
var pad, bbox;
|
|
|
|
// hide labels near the 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);
|
|
}
|
|
ids = utilArrayDifference(ids, context.mode()?.selectedIDs?.() || []);
|
|
|
|
layers.selectAll(utilEntitySelector(ids))
|
|
.classed('nolabel', true);
|
|
|
|
|
|
// draw the mouse bbox if debugging is on..
|
|
var debug = selection.selectAll('.labels-group.debug');
|
|
var gj = [];
|
|
if (context.getDebug('collision')) {
|
|
gj = bbox ? [{
|
|
type: 'Polygon',
|
|
coordinates: [[
|
|
[bbox.minX, bbox.minY],
|
|
[bbox.maxX, bbox.minY],
|
|
[bbox.maxX, bbox.maxY],
|
|
[bbox.minX, bbox.maxY],
|
|
[bbox.minX, bbox.minY]
|
|
]]
|
|
}] : [];
|
|
}
|
|
|
|
var box = debug.selectAll('.debug-mouse')
|
|
.data(gj);
|
|
|
|
// exit
|
|
box.exit()
|
|
.remove();
|
|
|
|
// enter/update
|
|
box.enter()
|
|
.append('path')
|
|
.attr('class', 'debug debug-mouse yellow')
|
|
.merge(box)
|
|
.attr('d', d3_geoPath());
|
|
}
|
|
|
|
|
|
var throttleFilterLabels = _throttle(filterLabels, 100);
|
|
|
|
|
|
drawLabels.observe = function(selection) {
|
|
var listener = function() { throttleFilterLabels(selection); };
|
|
selection.on('mousemove.hidelabels', listener);
|
|
context.on('enter.hidelabels', listener);
|
|
};
|
|
|
|
|
|
drawLabels.off = function(selection) {
|
|
throttleFilterLabels.cancel();
|
|
selection.on('mousemove.hidelabels', null);
|
|
context.on('enter.hidelabels', null);
|
|
};
|
|
|
|
|
|
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).filter(key =>
|
|
osmIsInterestingTag(key) &&
|
|
!nonPrimaryKeys.has(key) &&
|
|
!nonPrimaryKeysRegex.test(key)
|
|
);
|
|
return keys.length > 0 && keys.every(key =>
|
|
key.startsWith('addr:')
|
|
);
|
|
}
|