Files
iD/modules/svg/vertices.js
T
Bryan Housel f0a27bc1ec Simplify way segmentation and fix bug with adjacent segment type
(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)
2018-01-09 10:12:29 -05:00

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;
}