Files
iD/modules/svg/vertices.js
Bryan Housel be00a526b6 Make sure all targets are redrawn during a mode change
There was an issue where the lines did not redraw their targets right away
when entering drag node, which could make it possible for a quick drag node
to try to connect to its parent line.  With the chooseEdge exclusion it
would not connect to the parent nearby, but in another weird part of the line.
2017-12-22 11:42:21 -05:00

409 lines
15 KiB
JavaScript

import _assign from 'lodash-es/assign';
import _values from 'lodash-es/values';
import { select as d3_select } from 'd3-selection';
import { dataFeatureIcons } from '../../data';
import { geoScaleToZoom } from '../geo';
import { osmEntity } from '../osm';
import { svgPassiveVertex, svgPointTransform } from './index';
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);
function getIcon(entity) {
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.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);
});
});
selection.selectAll('use')
.attr('visibility', (z === 0 ? '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 icons get a `use`.
enter.filter(function(d) { return getIcon(d); })
.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 = dataFeatureIcons.indexOf(picon) !== -1;
return '#' + picon + (isMaki ? '-11' : '');
});
// 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); })
.call(updateAttributes);
// Directional vertices get viewfields
var dgroups = groups.filter(function(d) { return getDirections(d); })
.selectAll('.viewfieldgroup')
.data(function data(d) { return zoom < 18 ? [] : [d]; }, osmEntity.key);
// 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 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 activeID = context.activeID();
var data = { targets: [], nopes: [] };
entities.forEach(function(node) {
if (activeID === node.id) return; // draw no target on the activeID
var currType = svgPassiveVertex(node, graph, activeID);
if (currType !== 0) {
data.targets.push(node); // passive or adjacent - allow to connect
} else {
data.nopes.push({
id: node.id + '-nope', // not a real osmNode, break the id on purpose
originalID: node.id,
loc: node.loc
});
}
});
// Targets allow hover and vertex snapping
var targets = selection.selectAll('.vertex.target-allowed')
.filter(filter)
.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', svgPointTransform(projection));
// NOPE
var nopes = selection.selectAll('.vertex.target-nope')
.filter(function(d) { return filter({ id: d.originalID }); })
.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.id.replace('-nope','')] || radiuses.shadow[3]); })
.merge(nopes)
.attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; })
.attr('transform', svgPointTransform(projection));
}
// 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 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 zoom = geoScaleToZoom(projection.scale());
var mode = context.mode();
var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);
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))) {
_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 = _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);
};
selection.selectAll('.layer-points .layer-points-vertices')
.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);
};
selection.selectAll('.layer-points .layer-points-targets')
.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 || {};
_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, _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;
if (_currHoverTarget) {
_currHover = getSiblingAndChildVertices([_currHoverTarget.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, _values(_prevHover), filter, extent, false);
};
return drawVertices;
}