Files
iD/modules/svg/helpers.js
2025-03-20 14:01:26 +01:00

313 lines
10 KiB
JavaScript

import {
geoIdentity as d3_geoIdentity,
geoPath as d3_geoPath,
geoStream as d3_geoStream
} from 'd3-geo';
import { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';
// Touch targets control which other vertices we can drag a vertex onto.
//
// - the activeID - nope
// - 1 away (adjacent) to the activeID - yes (vertices will be merged)
// - 2 away from the activeID - nope (would create a self intersecting segment)
// - all others on a linear way - yes
// - all others on a closed way - nope (would create a self intersecting polygon)
//
// returns
// 0 = active vertex - no touch/connect
// 1 = passive vertex - yes touch/connect
// 2 = adjacent vertex - yes but pay attention segmenting a line here
//
export function svgPassiveVertex(node, graph, activeID) {
if (!activeID) return 1;
if (activeID === node.id) return 0;
var parents = graph.parentWays(node);
var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;
for (i = 0; i < parents.length; i++) {
nodes = parents[i].nodes;
isClosed = parents[i].isClosed();
for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby
if (nodes[j] === node.id) {
ix1 = j - 2;
ix2 = j - 1;
ix3 = j + 1;
ix4 = j + 2;
if (isClosed) { // wraparound if needed
max = nodes.length - 1;
if (ix1 < 0) ix1 = max + ix1;
if (ix2 < 0) ix2 = max + ix2;
if (ix3 > max) ix3 = ix3 - max;
if (ix4 > max) ix4 = ix4 - max;
}
if (nodes[ix1] === activeID) return 0; // no - prevent self intersect
else if (nodes[ix2] === activeID) return 2; // ok - adjacent
else if (nodes[ix3] === activeID) return 2; // ok - adjacent
else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect
else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect
}
}
}
return 1; // ok
}
/**
*
* @param {iD.Projection} projection
* @param {iD.Graph} graph
* @param {Number} dt spacing between segments
* @param {Function<Boolean>} [shouldReverse]
* @param {Function<Boolean>} [bothDirections]
*/
export function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {
/**
* @param {iD.OsmWay} entity
* @returns {[{id: String, d: String}]} list of svg path segments corres
*/
return function(entity) {
let i = 0;
let offset = dt / 2;
const segments = [];
const clip = paddedClipExtent(projection);
const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });
let a, b;
const _shouldReverse = shouldReverse(entity);
const _bothDirections = bothDirections(entity);
d3_geoStream({
type: 'LineString',
coordinates: coordinates
}, projection.stream(clip({
lineStart: function() {},
lineEnd: function() { a = null; },
point: function(x, y) {
b = [x, y];
if (a) {
let span = geoVecLength(a, b) - offset;
if (span >= 0) {
const heading = geoVecAngle(a, b);
const dx = dt * Math.cos(heading);
const dy = dt * Math.sin(heading);
let p = [
a[0] + offset * Math.cos(heading),
a[1] + offset * Math.sin(heading)
];
// gather coordinates
const coord = [a, p];
for (span -= dt; span >= 0; span -= dt) {
p = geoVecAdd(p, [dx, dy]);
coord.push(p);
}
coord.push(b);
// generate svg paths
let segment = '';
if (!_shouldReverse || _bothDirections) {
for (let j = 0; j < coord.length; j++) {
segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];
}
segments.push({ id: entity.id, index: i++, d: segment });
}
if (_shouldReverse || _bothDirections) {
segment = '';
for (let j = coord.length - 1; j >= 0; j--) {
segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];
}
segments.push({ id: entity.id, index: i++, d: segment });
}
}
offset = -span;
}
a = b;
}
})));
return segments;
};
}
/**
* @param {iD.Projection} projection
* @param {iD.Graph} graph
* @param {Boolean} isArea
*/
export function svgPath(projection, graph, isArea) {
const cache = {};
const project = projection.stream;
const clip = paddedClipExtent(projection, isArea);
const path = d3_geoPath()
.projection({stream: function(output) { return project(clip(output)); }});
const svgpath = function(entity) {
if (entity.id in cache) {
return cache[entity.id];
} else {
return cache[entity.id] = path(entity.asGeoJSON(graph));
}
};
svgpath.geojson = function(d) {
if (d.__featurehash__ !== undefined) {
if (d.__featurehash__ in cache) {
return cache[d.__featurehash__];
} else {
return cache[d.__featurehash__] = path(d);
}
} else {
return path(d);
}
};
return svgpath;
}
export function svgPointTransform(projection) {
var svgpoint = function(entity) {
// http://jsperf.com/short-array-join
var pt = projection(entity.loc);
return 'translate(' + pt[0] + ',' + pt[1] + ')';
};
svgpoint.geojson = function(d) {
return svgpoint(d.properties.entity);
};
return svgpoint;
}
export function svgRelationMemberTags(graph) {
return function(entity) {
var tags = entity.tags;
var shouldCopyMultipolygonTags = !entity.hasInterestingTags();
graph.parentRelations(entity).forEach(function(relation) {
var type = relation.tags.type;
if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {
tags = Object.assign({}, relation.tags, tags);
}
});
return tags;
};
}
export function svgSegmentWay(way, graph, activeID) {
// When there is no activeID, we can memoize this expensive computation
if (activeID === undefined) {
return graph.transient(way, 'waySegments', getWaySegments);
} else {
return getWaySegments();
}
function getWaySegments() {
var isActiveWay = (way.nodes.indexOf(activeID) !== -1);
var features = { passive: [], active: [] };
var start = {};
var end = {};
var node, type;
for (var i = 0; i < way.nodes.length; i++) {
node = graph.entity(way.nodes[i]);
type = svgPassiveVertex(node, graph, activeID);
end = { node: node, type: type };
if (start.type !== undefined) {
if (start.node.id === activeID || end.node.id === activeID) {
// push nothing
} else if (isActiveWay && (start.type === 2 || end.type === 2)) { // one adjacent vertex
pushActive(start, end, i);
} else if (start.type === 0 && end.type === 0) { // both active vertices
pushActive(start, end, i);
} else {
pushPassive(start, end, i);
}
}
start = end;
}
return features;
function pushActive(start, end, index) {
features.active.push({
type: 'Feature',
id: way.id + '-' + index + '-nope',
properties: {
nope: true,
target: true,
entity: way,
nodes: [start.node, end.node],
index: index
},
geometry: {
type: 'LineString',
coordinates: [start.node.loc, end.node.loc]
}
});
}
function pushPassive(start, end, index) {
features.passive.push({
type: 'Feature',
id: way.id + '-' + index,
properties: {
target: true,
entity: way,
nodes: [start.node, end.node],
index: index
},
geometry: {
type: 'LineString',
coordinates: [start.node.loc, end.node.loc]
}
});
}
}
}
/**
* Returns a d3 projection stream that clips the given geometries to an
* extent that is slightly padded.
*
* Explanation of magic numbers:
* "padding" here allows space for strokes to extend beyond the viewport,
* so that the stroke isn't drawn along the edge of the viewport when
* the shape is clipped.
* When drawing lines, pad viewport by 5px.
* When drawing areas, pad viewport by 65px in each direction to allow
* for 60px area fill stroke (see ".fill-partial path.fill" css rule)
*
* @param {import('../geo/raw_mercator').Projection} projection
* @param {Boolean} isArea
*/
function paddedClipExtent(projection, isArea = false) {
var padding = isArea ? 65 : 5;
var viewport = projection.clipExtent();
var paddedExtent = [
[viewport[0][0] - padding, viewport[0][1] - padding],
[viewport[1][0] + padding, viewport[1][1] + padding]
];
return d3_geoIdentity().clipExtent(paddedExtent).stream;
}