mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-13 04:44:50 +02:00
f0a27bc1ec
(closes #4669) Now instead of creating MultiLineString targets, we just create a bunch of LineString targets. This makes the code simpler, and anyway the entity is still there in `properties` for drawing code to decide what to do with the target. Incidentally, this change allows iD to support an extrusion operation. (Because each way segment has its own unique GeoJSON target now)
423 lines
15 KiB
JavaScript
423 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 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 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;
|
|
}
|