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

import deepEqual from 'fast-deep-equal'; import { select as d3_select } from 'd3-selection'; import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPassiveVertex, svgPointTransform } from './helpers'; export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, w/icon shadow: [6, 7.5, 7.5, 12], stroke: [2.5, 3.5, 3.5, 8], fill: [1, 1.5, 1.5, 1.5] }; var _currHoverTarget; var _currPersistent = {}; var _currHover = {}; var _prevHover = {}; var _currSelected = {}; var _prevSelected = {}; var _radii = {}; function sortY(a, b) { return b.loc[1] - a.loc[1]; } // Avoid exit/enter if we're just moving stuff around. // The node will get a new version but we only need to run the update selection. function fastEntityKey(d) { var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); return isMoving ? d.id : osmEntity.key(d); } function draw(selection, graph, vertices, sets, filter) { sets = sets || { selected: {}, important: {}, hovered: {} }; var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); var zoom = geoScaleToZoom(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); var activeID = context.activeID(); var base = context.history().base(); function getIcon(d) { // always check latest entity, as fastEntityKey avoids enter/exit now var entity = graph.entity(d.id); if (entity.id in icons) return icons[entity.id]; icons[entity.id] = entity.hasInterestingTags() && context.presets().match(entity, graph).icon; return icons[entity.id]; } // memoize directions results, return false for empty arrays (for use in filter) function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; var angles = entity.directions(graph, projection); directions[entity.id] = angles.length ? angles : false; return angles; } function updateAttributes(selection) { ['shadow', 'stroke', 'fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { var i = z && getIcon(entity); var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 if (entity.id !== activeID && entity.isEndpoint(graph) && !entity.isConnected(graph)) { r += 1.5; } if (klass === 'shadow') { // remember this value, so we don't need to _radii[entity.id] = r; // recompute it when we draw the touch targets } d3_select(this) .attr('r', r) .attr('visibility', (i && klass === 'fill') ? 'hidden' : null); }); }); } vertices.sort(sortY); var groups = selection.selectAll('g.vertex') .filter(filter) .data(vertices, fastEntityKey); // exit groups.exit() .remove(); // enter var enter = groups.enter() .append('g') .attr('class', function(d) { return 'node vertex ' + d.id; }) .order(); enter .append('circle') .attr('class', 'shadow'); enter .append('circle') .attr('class', 'stroke'); // Vertices with tags get a fill. enter.filter(function(d) { return d.hasInterestingTags(); }) .append('circle') .attr('class', 'fill'); // update groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) .classed('sibling', function(d) { return d.id in sets.selected; }) .classed('shared', function(d) { return graph.isShared(d); }) .classed('endpoint', function(d) { return d.isEndpoint(graph); }) .classed('added', function(d) { return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new }) .classed('moved', function(d) { return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc); }) .classed('retagged', function(d) { return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags); }) .call(updateAttributes); // Vertices with icons get a `use`. var iconUse = groups .selectAll('.icon') .data(function data(d) { return zoom >= 17 && getIcon(d) ? [d] : []; }, fastEntityKey); // exit iconUse.exit() .remove(); // enter iconUse.enter() .append('use') .attr('class', 'icon') .attr('width', '11px') .attr('height', '11px') .attr('transform', 'translate(-5.5, -5.5)') .attr('xlink:href', function(d) { var picon = getIcon(d); var isMaki = /^maki-/.test(picon); return '#' + picon + (isMaki ? '-11' : ''); }); // Vertices with directions get viewfields var dgroups = groups .selectAll('.viewfieldgroup') .data(function data(d) { return zoom >= 18 && getDirections(d) ? [d] : []; }, fastEntityKey); // exit dgroups.exit() .remove(); // enter/update dgroups = dgroups.enter() .insert('g', '.shadow') .attr('class', 'viewfieldgroup') .merge(dgroups); var viewfields = dgroups.selectAll('.viewfield') .data(getDirections, function key(d) { return osmEntity.key(d); }); // exit viewfields.exit() .remove(); // enter/update viewfields.enter() .append('path') .attr('class', 'viewfield') .attr('d', 'M0,0H0') .merge(viewfields) .attr('marker-start', 'url(#viewfield-marker' + (wireframe ? '-wireframe' : '') + ')') .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; var getTransform = svgPointTransform(projection).geojson; var activeID = context.activeID(); var data = { targets: [], nopes: [] }; entities.forEach(function(node) { if (activeID === node.id) return; // draw no target on the activeID var vertexType = svgPassiveVertex(node, graph, activeID); if (vertexType !== 0) { // passive or adjacent - allow to connect data.targets.push({ type: 'Feature', id: node.id, properties: { target: true, entity: node }, geometry: node.asGeoJSON() }); } else { data.nopes.push({ type: 'Feature', id: node.id + '-nope', properties: { nope: true, target: true, entity: node }, geometry: node.asGeoJSON() }); } }); // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() .remove(); // enter/update targets.enter() .append('circle') .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) .merge(targets) .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) .attr('transform', getTransform); // NOPE var nopes = selection.selectAll('.vertex.target-nope') .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit nopes.exit() .remove(); // enter/update nopes.enter() .append('circle') .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); }) .merge(nopes) .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) .attr('transform', getTransform); } // Points can also render as vertices: // 1. in wireframe mode or // 2. at higher zooms if they have a direction function renderAsVertex(entity, graph, wireframe, zoom) { var geometry = entity.geometry(graph); return geometry === 'vertex' || (geometry === 'point' && ( wireframe || (zoom >= 18 && entity.directions(graph, projection).length) )); } function isEditedNode(node, base, head) { var baseNode = base.entities[node.id]; var headNode = head.entities[node.id]; return !headNode || !baseNode || !deepEqual(headNode.tags, baseNode.tags) || !deepEqual(headNode.loc, baseNode.loc); } function getSiblingAndChildVertices(ids, graph, wireframe, zoom) { var results = {}; function addChildVertices(entity) { var geometry = entity.geometry(graph); if (!context.features().isHiddenFeature(entity, graph, geometry)) { var i; if (entity.type === 'way') { for (i = 0; i < entity.nodes.length; i++) { var child = graph.hasEntity(entity.nodes[i]); if (child) { addChildVertices(child); } } } else if (entity.type === 'relation') { for (i = 0; i < entity.members.length; i++) { var member = graph.hasEntity(entity.members[i].id); if (member) { addChildVertices(member); } } } else if (renderAsVertex(entity, graph, wireframe, zoom)) { results[entity.id] = entity; } } } ids.forEach(function(id) { var entity = graph.hasEntity(id); if (!entity) return; if (entity.type === 'node') { if (renderAsVertex(entity, graph, wireframe, zoom)) { results[entity.id] = entity; graph.parentWays(entity).forEach(function(entity) { addChildVertices(entity); }); } } else { // way, relation addChildVertices(entity); } }); return results; } function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); var visualDiff = context.surface().classed('highlight-edited'); var zoom = geoScaleToZoom(projection.scale()); var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); var base = context.history().base(); var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices'); var touchLayer = selection.selectAll('.layer-touch.points'); if (fullRedraw) { _currPersistent = {}; _radii = {}; } // Collect important vertices from the `entities` list.. // (during a paritial redraw, it will not contain everything) for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); var keep = false; // a point that looks like a vertex.. if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { _currPersistent[entity.id] = entity; keep = true; // a vertex of some importance.. } else if (geometry === 'vertex' && (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph) || (visualDiff && isEditedNode(entity, base, graph)))) { _currPersistent[entity.id] = entity; keep = true; } // whatever this is, it's not a persistent vertex.. if (!keep && !fullRedraw) { delete _currPersistent[entity.id]; } } // 3 sets of vertices to consider: var sets = { persistent: _currPersistent, // persistent = important vertices (render always) selected: _currSelected, // selected + siblings of selected (render always) hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; var all = Object.assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent); // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) // Adjust the filter function to expand the scope beyond whatever entities were passed in. var filterRendered = function(d) { return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; drawLayer .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. // When drawing, render all targets (not just those affected by a partial redraw) var filterTouch = function(d) { return isMoving ? true : filterRendered(d); }; touchLayer .call(drawTargets, graph, currentVisible(all), filterTouch); function currentVisible(which) { return Object.keys(which) .map(graph.hasEntity, graph) // the current version of this entity .filter(function (entity) { return entity && entity.intersects(extent, graph); }); } } // partial redraw - only update the selected items.. drawVertices.drawSelected = function(selection, graph, extent) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = geoScaleToZoom(projection.scale()); _prevSelected = _currSelected || {}; if (context.map().isInWideSelection()) { _currSelected = {}; context.selectedIDs().forEach(function(id) { var entity = graph.hasEntity(id); if (!entity) return; if (entity.type === 'node') { if (renderAsVertex(entity, graph, wireframe, zoom)) { _currSelected[entity.id] = entity; } } }); } else { _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); } // note that drawVertices will add `_currSelected` automatically if needed.. var filter = function(d) { return d.id in _prevSelected; }; drawVertices(selection, graph, Object.values(_prevSelected), filter, extent, false); }; // partial redraw - only update the hovered items.. drawVertices.drawHover = function(selection, graph, target, extent) { if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); var zoom = geoScaleToZoom(projection.scale()); _prevHover = _currHover || {}; _currHoverTarget = target; var entity = target && target.properties && target.properties.entity; if (entity) { _currHover = getSiblingAndChildVertices([entity.id], graph, wireframe, zoom); } else { _currHover = {}; } // note that drawVertices will add `_currHover` automatically if needed.. var filter = function(d) { return d.id in _prevHover; }; drawVertices(selection, graph, Object.values(_prevHover), filter, extent, false); }; return drawVertices; }