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} [shouldReverse] * @param {Function} [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; }