mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
This generalizes the oneway arrow logic for adding SVG markers along a line. Using that functionality, certain tags get arrows on their right-hand side, indicating which side is "inside", e.g. the right-side of a cliff is the lower side. The list of tags considered to be sided (unless there's a two_sided=yes tag) is: - natural=cliff - natural=coastline - barrier=retaining_wall - barrier=kerb - barrier=guard_rail - barrier=city_wall - man_made=embankment The triangles attempt to be reminiscent of the triangles used for rendering cliffs on OSM (and elsewhere). The different tags get different renderings (e.g. colors that match the main way, and different spacings). In addition, natural=coastline is special-cased to have blue markers (despite having a green way), to emphasise that the "inside" of a coastline is the water. Fixes https://github.com/openstreetmap/iD/issues/1475.
289 lines
9.3 KiB
JavaScript
289 lines
9.3 KiB
JavaScript
import _extend from 'lodash-es/extend';
|
|
|
|
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);
|
|
|
|
for (var i = 0; i < parents.length; i++) {
|
|
var nodes = parents[i].nodes;
|
|
var isClosed = parents[i].isClosed();
|
|
for (var j = 0; j < nodes.length; j++) { // find this vertex, look nearby
|
|
if (nodes[j] === node.id) {
|
|
var ix1 = j - 2;
|
|
var ix2 = j - 1;
|
|
var ix3 = j + 1;
|
|
var ix4 = j + 2;
|
|
|
|
if (isClosed) { // wraparound if needed
|
|
var 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
|
|
}
|
|
|
|
|
|
export function svgMarkerSegments(projection, graph, dt,
|
|
shouldReverse,
|
|
bothDirections) {
|
|
return function(entity) {
|
|
var i = 0;
|
|
var offset = dt;
|
|
var segments = [];
|
|
var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream;
|
|
var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });
|
|
var a, b;
|
|
|
|
if (shouldReverse(entity)) {
|
|
coordinates.reverse();
|
|
}
|
|
|
|
d3_geoStream({
|
|
type: 'LineString',
|
|
coordinates: coordinates
|
|
}, projection.stream(clip({
|
|
lineStart: function() {},
|
|
lineEnd: function() { a = null; },
|
|
point: function(x, y) {
|
|
b = [x, y];
|
|
|
|
if (a) {
|
|
var span = geoVecLength(a, b) - offset;
|
|
|
|
if (span >= 0) {
|
|
var heading = geoVecAngle(a, b);
|
|
var dx = dt * Math.cos(heading);
|
|
var dy = dt * Math.sin(heading);
|
|
var p = [
|
|
a[0] + offset * Math.cos(heading),
|
|
a[1] + offset * Math.sin(heading)
|
|
];
|
|
|
|
// gather coordinates
|
|
var coord = [a, p];
|
|
for (span -= dt; span >= 0; span -= dt) {
|
|
p = geoVecAdd(p, [dx, dy]);
|
|
coord.push(p);
|
|
}
|
|
coord.push(b);
|
|
|
|
// generate svg paths
|
|
var segment = '';
|
|
var j;
|
|
|
|
for (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 (bothDirections(entity)) {
|
|
segment = '';
|
|
for (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;
|
|
};
|
|
}
|
|
|
|
|
|
export function svgPath(projection, graph, isArea) {
|
|
|
|
// 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)
|
|
|
|
var cache = {};
|
|
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]
|
|
];
|
|
var clip = d3_geoIdentity().clipExtent(paddedExtent).stream;
|
|
var project = projection.stream;
|
|
var path = d3_geoPath()
|
|
.projection({stream: function(output) { return project(clip(output)); }});
|
|
|
|
var 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;
|
|
graph.parentRelations(entity).forEach(function(relation) {
|
|
var type = relation.tags.type;
|
|
if (type === 'multipolygon' || type === 'boundary') {
|
|
tags = _extend({}, 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]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|