modules/svg/lines.js (245 lines of code) (raw):

import deepEqual from 'fast-deep-equal'; import { range as d3_range } from 'd3-array'; import { svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay } from './helpers'; import { svgTagClasses } from './tag_classes'; import { osmEntity, osmOldMultipolygonOuterMember } from '../osm'; import { utilArrayFlatten, utilArrayGroupBy } from '../util'; import { utilDetect } from '../util/detect'; export function svgLines(projection, context) { var detected = utilDetect(); var highway_stack = { motorway: 0, motorway_link: 1, trunk: 2, trunk_link: 3, primary: 4, primary_link: 5, secondary: 6, tertiary: 7, unclassified: 8, residential: 9, service: 10, footway: 11 }; function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; var getPath = svgPath(projection).geojson; var activeID = context.activeID(); var base = context.history().base(); // The targets and nopes will be MultiLineString sub-segments of the ways var data = { targets: [], nopes: [] }; entities.forEach(function(way) { var features = svgSegmentWay(way, graph, activeID); data.targets.push.apply(data.targets, features.passive); data.nopes.push.apply(data.nopes, features.active); }); // Targets allow hover and vertex snapping var targetData = data.targets.filter(getPath); var targets = selection.selectAll('.line.target-allowed') .filter(function(d) { return filter(d.properties.entity); }) .data(targetData, function key(d) { return d.id; }); // exit targets.exit() .remove(); var segmentWasEdited = function(d) { var wayID = d.properties.entity.id; // if the whole line was edited, don't draw segment changes if (!base.entities[wayID] || !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) { return false; } return d.properties.nodes.some(function(n) { return !base.entities[n.id] || !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc); }); }; // enter/update targets.enter() .append('path') .merge(targets) .attr('d', getPath) .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }) .classed('segment-edited', segmentWasEdited); // NOPE var nopeData = data.nopes.filter(getPath); var nopes = selection.selectAll('.line.target-nope') .filter(function(d) { return filter(d.properties.entity); }) .data(nopeData, function key(d) { return d.id; }); // exit nopes.exit() .remove(); // enter/update nopes.enter() .append('path') .merge(nopes) .attr('d', getPath) .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }) .classed('segment-edited', segmentWasEdited); } function drawLines(selection, graph, entities, filter) { var base = context.history().base(); function waystack(a, b) { var selected = context.selectedIDs(); var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0; var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; } if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; } return scoreA - scoreB; } function drawLineGroup(selection, klass, isSelected) { // Note: Don't add `.selected` class in draw modes var mode = context.mode(); var isDrawing = mode && /^draw/.test(mode.id); var selectedClass = (!isDrawing && isSelected) ? 'selected ' : ''; var lines = selection .selectAll('path') .filter(filter) .data(getPathData(isSelected), osmEntity.key); lines.exit() .remove(); // Optimization: Call expensive TagClasses only on enter selection. This // works because osmEntity.key is defined to include the entity v attribute. lines.enter() .append('path') .attr('class', function(d) { var prefix = 'way line'; if (!d.hasInterestingTags() && graph.parentMultipolygons(d).length > 0) { // fudge the classes to style multipolygon member lines as area edges prefix = 'relation area'; } var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : ''; return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id; }) .classed('added', function(d) { return !base.entities[d.id]; }) .classed('geometry-edited', function(d) { return graph.entities[d.id] && base.entities[d.id] && !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes); }) .classed('retagged', function(d) { return graph.entities[d.id] && base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags); }) .call(svgTagClasses()) .merge(lines) .sort(waystack) .attr('d', getPath) .call(svgTagClasses().tags(svgRelationMemberTags(graph))); return selection; } function getPathData(isSelected) { return function() { var layer = this.parentNode.__data__; var data = pathdata[layer] || []; return data.filter(function(d) { if (isSelected) return context.selectedIDs().indexOf(d.id) !== -1; else return context.selectedIDs().indexOf(d.id) === -1; }); }; } function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) { var markergroup = layergroup .selectAll('g.' + groupclass) .data([pathclass]); markergroup = markergroup.enter() .append('g') .attr('class', groupclass) .merge(markergroup); var markers = markergroup .selectAll('path') .filter(filter) .data( function data() { return groupdata[this.parentNode.__data__] || []; }, function key(d) { return [d.id, d.index]; } ); markers.exit() .remove(); markers = markers.enter() .append('path') .attr('class', pathclass) .merge(markers) .attr('marker-mid', marker) .attr('d', function(d) { return d.d; }); if (detected.ie) { markers.each(function() { this.parentNode.insertBefore(this, this); }); } } var getPath = svgPath(projection, graph); var ways = []; var onewaydata = {}; var sideddata = {}; var oldMultiPolygonOuters = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var outer = osmOldMultipolygonOuterMember(entity, graph); if (outer) { ways.push(entity.mergeTags(outer.tags)); oldMultiPolygonOuters[outer.id] = true; } else if (entity.geometry(graph) === 'line') { ways.push(entity); } } ways = ways.filter(getPath); var pathdata = utilArrayGroupBy(ways, function(way) { return way.layer(); }); Object.keys(pathdata).forEach(function(k) { var v = pathdata[k]; var onewayArr = v.filter(function(d) { return d.isOneWay(); }); var onewaySegments = svgMarkerSegments( projection, graph, 35, function shouldReverse(entity) { return entity.tags.oneway === '-1'; }, function bothDirections(entity) { return entity.tags.oneway === 'reversible' || entity.tags.oneway === 'alternating'; } ); onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments)); var sidedArr = v.filter(function(d) { return d.isSided(); }); var sidedSegments = svgMarkerSegments( projection, graph, 30, function shouldReverse() { return false; }, function bothDirections() { return false; } ); sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments)); }); var covered = selection.selectAll('.layer-osm.covered'); // under areas var uncovered = selection.selectAll('.layer-osm.lines'); // over areas var touchLayer = selection.selectAll('.layer-touch.lines'); // Draw lines.. [covered, uncovered].forEach(function(selection) { var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11)); var layergroup = selection .selectAll('g.layergroup') .data(range); layergroup = layergroup.enter() .append('g') .attr('class', function(d) { return 'layergroup layer' + String(d); }) .merge(layergroup); layergroup .selectAll('g.linegroup') .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted']) .enter() .append('g') .attr('class', function(d) { return 'linegroup line-' + d; }); layergroup.selectAll('g.line-shadow') .call(drawLineGroup, 'shadow', false); layergroup.selectAll('g.line-casing') .call(drawLineGroup, 'casing', false); layergroup.selectAll('g.line-stroke') .call(drawLineGroup, 'stroke', false); layergroup.selectAll('g.line-shadow-highlighted') .call(drawLineGroup, 'shadow', true); layergroup.selectAll('g.line-casing-highlighted') .call(drawLineGroup, 'casing', true); layergroup.selectAll('g.line-stroke-highlighted') .call(drawLineGroup, 'stroke', true); addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, 'url(#oneway-marker)'); addMarkers(layergroup, 'sided', 'sidedgroup', sideddata, function marker(d) { var category = graph.entity(d.id).sidednessIdentifier(); return 'url(#sided-marker-' + category + ')'; } ); }); // Draw touch targets.. touchLayer .call(drawTargets, graph, ways, filter); } return drawLines; }