mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-01 08:53:41 +00:00
(closes #4669) Now instead of creating MultiLineString targets, we just create a bunch of LineString targets. This makes the code simpler, and anyway the entity is still there in `properties` for drawing code to decide what to do with the target. Incidentally, this change allows iD to support an extrusion operation. (Because each way segment has its own unique GeoJSON target now)
270 lines
8.4 KiB
JavaScript
270 lines
8.4 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 svgOneWaySegments(projection, graph, dt) {
|
|
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 (entity.tags.oneway === '-1') {
|
|
coordinates.reverse();
|
|
}
|
|
|
|
var isReversible = (entity.tags.oneway === 'reversible' || entity.tags.oneway === 'alternating');
|
|
|
|
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 (isReversible) {
|
|
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 = path;
|
|
|
|
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) {
|
|
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 (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]
|
|
}
|
|
});
|
|
}
|
|
}
|