Files
iD/modules/svg/helpers.js
Bryan Housel f0a27bc1ec Simplify way segmentation and fix bug with adjacent segment type
(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)
2018-01-09 10:12:29 -05:00

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