mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-11 21:56:00 +00:00
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.
409 lines
15 KiB
JavaScript
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;
|
|
}
|