Merge pull request #4602 from openstreetmap/direction_vertices

Support rendering `direction` on vertices (stop sign, traffic_signals, etc)
This commit is contained in:
Bryan Housel
2018-01-01 23:02:45 -05:00
committed by GitHub
109 changed files with 4820 additions and 2720 deletions
+8 -12
View File
@@ -10,11 +10,7 @@ import {
polygonCentroid as d3_polygonCentroid
} from 'd3-polygon';
import {
geoEuclideanDistance,
geoInterp
} from '../geo';
import { geoVecInterp, geoVecLength } from '../geo';
import { osmNode } from '../osm';
@@ -41,8 +37,8 @@ export function actionCircularize(wayId, projection, maxAngle) {
keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }),
points = nodes.map(function(n) { return projection(n.loc); }),
keyPoints = keyNodes.map(function(n) { return projection(n.loc); }),
centroid = (points.length === 2) ? geoInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points),
radius = d3_median(points, function(p) { return geoEuclideanDistance(centroid, p); }),
centroid = (points.length === 2) ? geoVecInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points),
radius = d3_median(points, function(p) { return geoVecLength(centroid, p); }),
sign = d3_polygonArea(points) > 0 ? 1 : -1,
ids;
@@ -82,7 +78,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
}
// position this key node
var distance = geoEuclideanDistance(centroid, keyPoints[i]);
var distance = geoVecLength(centroid, keyPoints[i]);
if (distance === 0) { distance = 1e-4; }
keyPoints[i] = [
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
@@ -91,7 +87,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
loc = projection.invert(keyPoints[i]);
node = keyNodes[i];
origNode = origNodes[node.id];
node = node.move(geoInterp(origNode.loc, loc, t));
node = node.move(geoVecInterp(origNode.loc, loc, t));
graph = graph.replace(node);
// figure out the between delta angle we want to match to
@@ -122,7 +118,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
origNode = origNodes[node.id];
nearNodes[node.id] = angle;
node = node.move(geoInterp(origNode.loc, loc, t));
node = node.move(geoVecInterp(origNode.loc, loc, t));
graph = graph.replace(node);
}
@@ -145,7 +141,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
}
}
node = osmNode({ loc: geoInterp(origNode.loc, loc, t) });
node = osmNode({ loc: geoVecInterp(origNode.loc, loc, t) });
graph = graph.replace(node);
nodes.splice(endNodeIndex + j, 0, node);
@@ -220,7 +216,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
// move interior nodes to the surface of the convex hull..
for (var j = 1; j < indexRange; j++) {
var point = geoInterp(hull[i], hull[i+1], j / indexRange),
var point = geoVecInterp(hull[i], hull[i+1], j / indexRange),
node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
graph = graph.replace(node);
}
+46 -46
View File
@@ -11,22 +11,21 @@ import _without from 'lodash-es/without';
import { osmNode } from '../osm';
import {
geoChooseEdge,
geoAngle,
geoInterp,
geoChooseEdge,
geoPathIntersections,
geoPathLength,
geoSphericalDistance
geoSphericalDistance,
geoVecAdd,
geoVecInterp,
geoVecSubtract
} from '../geo';
// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
export function actionMove(moveIds, tryDelta, projection, cache) {
var delta = tryDelta;
function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
var _delta = tryDelta;
function setupCache(graph) {
function canMove(nodeId) {
@@ -118,11 +117,11 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
// Place a vertex where the moved vertex used to be, to preserve way shape..
function replaceMovedVertex(nodeId, wayId, graph, delta) {
var way = graph.entity(wayId),
moved = graph.entity(nodeId),
movedIndex = way.nodes.indexOf(nodeId),
len, prevIndex, nextIndex;
function replaceMovedVertex(nodeId, wayId, graph, _delta) {
var way = graph.entity(wayId);
var moved = graph.entity(nodeId);
var movedIndex = way.nodes.indexOf(nodeId);
var len, prevIndex, nextIndex;
if (way.isClosed()) {
len = way.nodes.length - 1;
@@ -134,14 +133,14 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
nextIndex = movedIndex + 1;
}
var prev = graph.hasEntity(way.nodes[prevIndex]),
next = graph.hasEntity(way.nodes[nextIndex]);
var prev = graph.hasEntity(way.nodes[prevIndex]);
var next = graph.hasEntity(way.nodes[nextIndex]);
// Don't add orig vertex at endpoint..
if (!prev || !next) return graph;
var key = wayId + '_' + nodeId,
orig = cache.replacedVertex[key];
var key = wayId + '_' + nodeId;
var orig = cache.replacedVertex[key];
if (!orig) {
orig = osmNode();
cache.replacedVertex[key] = orig;
@@ -149,9 +148,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
}
var start, end;
if (delta) {
if (_delta) {
start = projection(cache.startLoc[nodeId]);
end = projection.invert(vecAdd(start, delta));
end = projection.invert(geoVecAdd(start, _delta));
} else {
end = cache.startLoc[nodeId];
}
@@ -184,30 +183,30 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
// Reorder nodes around intersections that have moved..
function unZorroIntersection(intersection, graph) {
var vertex = graph.entity(intersection.nodeId),
way1 = graph.entity(intersection.movedId),
way2 = graph.entity(intersection.unmovedId),
isEP1 = intersection.movedIsEP,
isEP2 = intersection.unmovedIsEP;
var vertex = graph.entity(intersection.nodeId);
var way1 = graph.entity(intersection.movedId);
var way2 = graph.entity(intersection.unmovedId);
var isEP1 = intersection.movedIsEP;
var isEP2 = intersection.unmovedIsEP;
// don't move the vertex if it is the endpoint of both ways.
if (isEP1 && isEP2) return graph;
var nodes1 = _without(graph.childNodes(way1), vertex),
nodes2 = _without(graph.childNodes(way2), vertex);
var nodes1 = _without(graph.childNodes(way1), vertex);
var nodes2 = _without(graph.childNodes(way2), vertex);
if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]);
if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]);
var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection),
edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection),
loc;
var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection);
var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection);
var loc;
// snap vertex to nearest edge (or some point between them)..
if (!isEP1 && !isEP2) {
var epsilon = 1e-4, maxIter = 10;
for (var i = 0; i < maxIter; i++) {
loc = geoInterp(edge1.loc, edge2.loc, 0.5);
loc = geoVecInterp(edge1.loc, edge2.loc, 0.5);
edge1 = geoChooseEdge(nodes1, projection(loc), projection);
edge2 = geoChooseEdge(nodes2, projection(loc), projection);
if (Math.abs(edge1.distance - edge2.distance) < epsilon) break;
@@ -236,7 +235,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
function cleanupIntersections(graph) {
_each(cache.intersection, function(obj) {
graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta);
graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta);
graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null);
graph = unZorroIntersection(obj, graph);
});
@@ -245,7 +244,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
}
// check if moving way endpoint can cross an unmoved way, if so limit delta..
// check if moving way endpoint can cross an unmoved way, if so limit _delta..
function limitDelta(graph) {
_each(cache.intersection, function(obj) {
// Don't limit movement if this is vertex joins 2 endpoints..
@@ -253,27 +252,28 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
// Don't limit movement if this vertex is not an endpoint anyway..
if (!obj.movedIsEP) return;
var node = graph.entity(obj.nodeId),
start = projection(node.loc),
end = vecAdd(start, delta),
movedNodes = graph.childNodes(graph.entity(obj.movedId)),
movedPath = _map(_map(movedNodes, 'loc'),
function(loc) { return vecAdd(projection(loc), delta); }),
unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)),
unmovedPath = _map(_map(unmovedNodes, 'loc'), projection),
hits = geoPathIntersections(movedPath, unmovedPath);
var node = graph.entity(obj.nodeId);
var start = projection(node.loc);
var end = geoVecAdd(start, _delta);
var movedNodes = graph.childNodes(graph.entity(obj.movedId));
var movedPath = _map(_map(movedNodes, 'loc'), function(loc) {
return geoVecAdd(projection(loc), _delta);
});
var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId));
var unmovedPath = _map(_map(unmovedNodes, 'loc'), projection);
var hits = geoPathIntersections(movedPath, unmovedPath);
for (var i = 0; i < hits.length; i++) {
if (_isEqual(hits[i], end)) continue;
var edge = geoChooseEdge(unmovedNodes, end, projection);
delta = vecSub(projection(edge.loc), start);
_delta = geoVecSubtract(projection(edge.loc), start);
}
});
}
var action = function(graph) {
if (delta[0] === 0 && delta[1] === 0) return graph;
if (_delta[0] === 0 && _delta[1] === 0) return graph;
setupCache(graph);
@@ -282,9 +282,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
}
_each(cache.nodes, function(id) {
var node = graph.entity(id),
start = projection(node.loc),
end = vecAdd(start, delta);
var node = graph.entity(id);
var start = projection(node.loc);
var end = geoVecAdd(start, _delta);
graph = graph.replace(node.move(projection.invert(end)));
});
@@ -297,7 +297,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
action.delta = function() {
return delta;
return _delta;
};
+16 -5
View File
@@ -1,7 +1,18 @@
// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
export function actionMoveNode(nodeId, loc) {
return function(graph) {
return graph.replace(graph.entity(nodeId).move(loc));
import { geoVecInterp } from '../geo';
export function actionMoveNode(nodeID, toLoc) {
var action = function(graph, t) {
if (t === null || !isFinite(t)) t = 1;
t = Math.min(Math.max(+t, 0), 1);
var node = graph.entity(nodeID);
return graph.replace(
node.move(geoVecInterp(node.loc, toLoc, t))
);
};
action.transitionable = true;
return action;
}
+4 -7
View File
@@ -2,10 +2,7 @@ import _clone from 'lodash-es/clone';
import _uniq from 'lodash-es/uniq';
import { actionDeleteNode } from './delete_node';
import {
geoEuclideanDistance,
geoInterp
} from '../geo';
import { geoVecInterp, geoVecLength } from '../geo';
/*
@@ -40,7 +37,7 @@ export function actionOrthogonalize(wayId, projection) {
node = graph.entity(nodes[corner.i].id);
loc = projection.invert(points[corner.i]);
graph = graph.replace(node.move(geoInterp(node.loc, loc, t)));
graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));
} else {
var best,
@@ -69,7 +66,7 @@ export function actionOrthogonalize(wayId, projection) {
if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) {
loc = projection.invert(points[i]);
node = graph.entity(nodes[i].id);
graph = graph.replace(node.move(geoInterp(node.loc, loc, t)));
graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));
}
}
@@ -100,7 +97,7 @@ export function actionOrthogonalize(wayId, projection) {
q = subtractPoints(c, b),
scale, dotp;
scale = 2 * Math.min(geoEuclideanDistance(p, [0, 0]), geoEuclideanDistance(q, [0, 0]));
scale = 2 * Math.min(geoVecLength(p, [0, 0]), geoVecLength(q, [0, 0]));
p = normalizePoint(p, 1.0);
q = normalizePoint(q, 1.0);
+5 -5
View File
@@ -4,10 +4,10 @@ import {
} from 'd3-polygon';
import {
geoEuclideanDistance,
geoExtent,
geoInterp,
geoRotate
geoRotate,
geoVecInterp,
geoVecLength
} from '../geo';
import { utilGetAllNodes } from '../util';
@@ -69,7 +69,7 @@ export function actionReflect(reflectIds, projection) {
q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ],
p, q;
var isLong = (geoEuclideanDistance(p1, q1) > geoEuclideanDistance(p2, q2));
var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2));
if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) {
p = p1;
q = q1;
@@ -92,7 +92,7 @@ export function actionReflect(reflectIds, projection) {
b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1]
];
var loc2 = projection.invert(c2);
node = node.move(geoInterp(node.loc, loc2, t));
node = node.move(geoVecInterp(node.loc, loc2, t));
graph = graph.replace(node);
}
+2 -1
View File
@@ -87,7 +87,8 @@ export function actionReverse(wayId, options) {
// Update the direction based tags as appropriate then return an updated node
return node.update({tags: _transform(node.tags, function(acc, tagValue, tagKey) {
// See if this is a direction tag and reverse (or use existing value if not recognised)
if (tagKey.match(/direction$/) !== null) {
var re = /direction$/;
if (re.test(tagKey)) {
acc[tagKey] = {forward: 'backward', backward: 'forward', left: 'right', right: 'left'}[tagValue] || tagValue;
} else {
// Use the reverseKey method to cater for situations such as traffic_sign:forward=stop
+4 -4
View File
@@ -1,8 +1,8 @@
import { actionDeleteNode } from './delete_node';
import {
geoEuclideanDistance,
geoInterp
geoVecInterp,
geoVecLength
} from '../geo';
@@ -44,7 +44,7 @@ export function actionStraighten(wayId, projection) {
],
loc2 = projection.invert(p);
graph = graph.replace(node.move(geoInterp(node.loc, loc2, t)));
graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t)));
} else {
// safe to delete
@@ -69,7 +69,7 @@ export function actionStraighten(wayId, projection) {
points = nodes.map(function(n) { return projection(n.loc); }),
startPoint = points[0],
endPoint = points[points.length-1],
threshold = 0.2 * geoEuclideanDistance(startPoint, endPoint),
threshold = 0.2 * geoVecLength(startPoint, endPoint),
i;
if (threshold === 0) {
+61 -66
View File
@@ -33,17 +33,18 @@ import {
*/
export function behaviorDrag() {
var event = d3_dispatch('start', 'move', 'end'),
origin = null,
selector = '',
filter = null,
event_, target, surface;
var dispatch = d3_dispatch('start', 'move', 'end');
var _origin = null;
var _selector = '';
var _event;
var _target;
var _surface;
var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'),
d3_event_userSelectSuppress = function() {
var selection = d3_selection(),
select = selection.style(d3_event_userSelectProperty);
var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect');
var d3_event_userSelectSuppress = function() {
var selection = d3_selection();
var select = selection.style(d3_event_userSelectProperty);
selection.style(d3_event_userSelectProperty, 'none');
return function() {
selection.style(d3_event_userSelectProperty, select);
@@ -60,29 +61,29 @@ export function behaviorDrag() {
function eventOf(thiz, argumentz) {
return function(e1) {
e1.target = drag;
d3_customEvent(e1, event.apply, event, [e1.type, thiz, argumentz]);
d3_customEvent(e1, dispatch.apply, dispatch, [e1.type, thiz, argumentz]);
};
}
function dragstart() {
target = this;
event_ = eventOf(target, arguments);
_target = this;
_event = eventOf(_target, arguments);
var eventTarget = d3_event.target,
touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null,
offset,
origin_ = point(),
started = false,
selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag');
var eventTarget = d3_event.target;
var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null;
var offset;
var startOrigin = point();
var started = false;
var selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag');
d3_select(window)
.on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove)
.on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true);
if (origin) {
offset = origin.apply(target, arguments);
offset = [offset[0] - origin_[0], offset[1] - origin_[1]];
if (_origin) {
offset = _origin.apply(_target, arguments);
offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]];
} else {
offset = [0, 0];
}
@@ -93,7 +94,7 @@ export function behaviorDrag() {
function point() {
var p = surface || target.parentNode;
var p = _surface || _target.parentNode;
return touchId !== null ? d3_touches(p).filter(function(p) {
return p.identifier === touchId;
})[0] : d3_mouse(p);
@@ -101,32 +102,32 @@ export function behaviorDrag() {
function dragmove() {
var p = point(),
dx = p[0] - origin_[0],
dy = p[1] - origin_[1];
var p = point();
var dx = p[0] - startOrigin[0];
var dy = p[1] - startOrigin[1];
if (dx === 0 && dy === 0)
return;
if (!started) {
started = true;
event_({ type: 'start' });
}
origin_ = p;
startOrigin = p;
d3_eventCancel();
event_({
type: 'move',
point: [p[0] + offset[0], p[1] + offset[1]],
delta: [dx, dy]
});
if (!started) {
started = true;
_event({ type: 'start' });
} else {
_event({
type: 'move',
point: [p[0] + offset[0], p[1] + offset[1]],
delta: [dx, dy]
});
}
}
function dragend() {
if (started) {
event_({ type: 'end' });
_event({ type: 'end' });
d3_eventCancel();
if (d3_event.target === eventTarget) {
@@ -152,52 +153,46 @@ export function behaviorDrag() {
function drag(selection) {
var matchesSelector = utilPrefixDOMProperty('matchesSelector'),
delegate = dragstart;
var matchesSelector = utilPrefixDOMProperty('matchesSelector');
var delegate = dragstart;
if (selector) {
if (_selector) {
delegate = function() {
var root = this,
target = d3_event.target;
var root = this;
var target = d3_event.target;
for (; target && target !== root; target = target.parentNode) {
if (target[matchesSelector](selector) &&
(!filter || filter(target.__data__))) {
return dragstart.call(target, target.__data__);
var datum = target.__data__;
var entity = datum && datum.properties && datum.properties.entity;
if (entity && target[matchesSelector](_selector)) {
return dragstart.call(target, entity);
}
}
};
}
selection
.on('mousedown.drag' + selector, delegate)
.on('touchstart.drag' + selector, delegate);
.on('mousedown.drag' + _selector, delegate)
.on('touchstart.drag' + _selector, delegate);
}
drag.off = function(selection) {
selection
.on('mousedown.drag' + selector, null)
.on('touchstart.drag' + selector, null);
.on('mousedown.drag' + _selector, null)
.on('touchstart.drag' + _selector, null);
};
drag.selector = function(_) {
if (!arguments.length) return selector;
selector = _;
return drag;
};
drag.filter = function(_) {
if (!arguments.length) return origin;
filter = _;
if (!arguments.length) return _selector;
_selector = _;
return drag;
};
drag.origin = function (_) {
if (!arguments.length) return origin;
origin = _;
if (!arguments.length) return _origin;
_origin = _;
return drag;
};
@@ -211,19 +206,19 @@ export function behaviorDrag() {
drag.target = function() {
if (!arguments.length) return target;
target = arguments[0];
event_ = eventOf(target, Array.prototype.slice.call(arguments, 1));
if (!arguments.length) return _target;
_target = arguments[0];
_event = eventOf(_target, Array.prototype.slice.call(arguments, 1));
return drag;
};
drag.surface = function() {
if (!arguments.length) return surface;
surface = arguments[0];
if (!arguments.length) return _surface;
_surface = arguments[0];
return drag;
};
return utilRebind(drag, event, 'on');
return utilRebind(drag, dispatch, 'on');
}
+72 -56
View File
@@ -14,40 +14,52 @@ import { behaviorTail } from './tail';
import {
geoChooseEdge,
geoEuclideanDistance
geoVecLength,
geoViewportEdge
} from '../geo';
import { utilRebind } from '../util/rebind';
var usedTails = {};
var disableSpace = false;
var lastSpace = null;
var _usedTails = {};
var _disableSpace = false;
var _lastSpace = null;
export function behaviorDraw(context) {
var dispatch = d3_dispatch('move', 'click', 'clickWay',
'clickNode', 'undo', 'cancel', 'finish'),
keybinding = d3_keybinding('draw'),
hover = behaviorHover(context)
.altDisables(true)
.on('hover', context.ui().sidebar.hover),
tail = behaviorTail(),
edit = behaviorEdit(context),
closeTolerance = 4,
tolerance = 12,
mouseLeave = false,
lastMouse = null;
var dispatch = d3_dispatch(
'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish'
);
var keybinding = d3_keybinding('draw');
var hover = behaviorHover(context).altDisables(true)
.on('hover', context.ui().sidebar.hover);
var tail = behaviorTail();
var edit = behaviorEdit(context);
var closeTolerance = 4;
var tolerance = 12;
var _mouseLeave = false;
var _lastMouse = null;
// related code
// - `mode/drag_node.js` `datum()`
function datum() {
if (d3_event.altKey) return {};
var element;
if (d3_event.type === 'keydown') {
return (lastMouse && lastMouse.target.__data__) || {};
element = _lastMouse && _lastMouse.target;
} else {
return d3_event.target.__data__ || {};
element = d3_event.target;
}
// When drawing, snap only to touch targets..
// (this excludes area fills and active drawing elements)
var d = element.__data__;
return (d && d.properties && d.properties.target) ? d : {};
}
@@ -60,17 +72,17 @@ export function behaviorDraw(context) {
})[0] : d3_mouse(p);
}
var element = d3_select(this),
touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null,
t1 = +new Date(),
p1 = point();
var element = d3_select(this);
var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null;
var t1 = +new Date();
var p1 = point();
element.on('mousemove.draw', null);
d3_select(window).on('mouseup.draw', function() {
var t2 = +new Date(),
p2 = point(),
dist = geoEuclideanDistance(p1, p2);
var t2 = +new Date();
var p2 = point();
var dist = geoVecLength(p1, p2);
element.on('mousemove.draw', mousemove);
d3_select(window).on('mouseup.draw', null);
@@ -95,44 +107,48 @@ export function behaviorDraw(context) {
function mousemove() {
lastMouse = d3_event;
_lastMouse = d3_event;
dispatch.call('move', this, datum());
}
function mouseenter() {
mouseLeave = false;
_mouseLeave = false;
}
function mouseleave() {
mouseLeave = true;
_mouseLeave = true;
}
// related code
// - `mode/drag_node.js` `doMode()`
// - `behavior/draw.js` `click()`
// - `behavior/draw_way.js` `move()`
function click() {
var d = datum();
if (d.type === 'way') {
var dims = context.map().dimensions(),
mouse = context.mouse(),
pad = 5,
trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
mouse[1] > pad && mouse[1] < dims[1] - pad;
var target = d && d.id && context.hasEntity(d.id);
if (trySnap) {
var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection),
edge = [d.nodes[choice.index - 1], d.nodes[choice.index]];
dispatch.call('clickWay', this, choice.loc, edge);
} else {
dispatch.call('click', this, context.map().mouseCoordinates());
var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null;
if (trySnap) {
if (target && target.type === 'node') { // Snap to a node
dispatch.call('clickNode', this, target);
return;
} else if (target && target.type === 'way') { // Snap to a way
var choice = geoChooseEdge(
context.childNodes(target), context.mouse(), context.projection, context.activeID()
);
if (choice) {
var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]];
dispatch.call('clickWay', this, choice.loc, edge);
return;
}
}
} else if (d.type === 'node') {
dispatch.call('clickNode', this, d);
} else {
dispatch.call('click', this, context.map().mouseCoordinates());
}
dispatch.call('click', this, context.map().mouseCoordinates(), d);
}
@@ -141,23 +157,23 @@ export function behaviorDraw(context) {
d3_event.stopPropagation();
var currSpace = context.mouse();
if (disableSpace && lastSpace) {
var dist = geoEuclideanDistance(lastSpace, currSpace);
if (_disableSpace && _lastSpace) {
var dist = geoVecLength(_lastSpace, currSpace);
if (dist > tolerance) {
disableSpace = false;
_disableSpace = false;
}
}
if (disableSpace || mouseLeave || !lastMouse) return;
if (_disableSpace || _mouseLeave || !_lastMouse) return;
// user must move mouse or release space bar to allow another click
lastSpace = currSpace;
disableSpace = true;
_lastSpace = currSpace;
_disableSpace = true;
d3_select(window).on('keyup.space-block', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
disableSpace = false;
_disableSpace = false;
d3_select(window).on('keyup.space-block', null);
});
@@ -187,7 +203,7 @@ export function behaviorDraw(context) {
context.install(hover);
context.install(edit);
if (!context.inIntro() && !usedTails[tail.text()]) {
if (!context.inIntro() && !_usedTails[tail.text()]) {
context.install(tail);
}
@@ -217,9 +233,9 @@ export function behaviorDraw(context) {
context.uninstall(hover);
context.uninstall(edit);
if (!context.inIntro() && !usedTails[tail.text()]) {
if (!context.inIntro() && !_usedTails[tail.text()]) {
context.uninstall(tail);
usedTails[tail.text()] = true;
_usedTails[tail.text()] = true;
}
selection
+118 -168
View File
@@ -1,97 +1,87 @@
import _clone from 'lodash-es/clone';
import { t } from '../util/locale';
import {
actionAddEntity,
actionAddMidpoint,
actionMoveNode,
actionNoop
} from '../actions';
import { behaviorDraw } from './draw';
import {
geoChooseEdge,
geoEdgeEqual
} from '../geo';
import {
modeBrowse,
modeSelect
} from '../modes';
import {
osmNode,
osmWay
} from '../osm';
import { utilEntitySelector } from '../util';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';
export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
var origWay = context.entity(wayId);
var annotation = t((origWay.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)
);
var behavior = behaviorDraw(context);
var _tempEdits = 0;
var origWay = context.entity(wayId),
isArea = context.geometry(wayId) === 'area',
tempEdits = 0,
annotation = t((origWay.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)),
draw = behaviorDraw(context),
startIndex,
start,
end,
segment;
// initialize the temporary drawing entities
if (!isArea) {
startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0;
start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc });
end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() });
segment = osmWay({ id: 'wTemp',
nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id],
tags: _clone(origWay.tags)
});
} else {
end = osmNode({ loc: context.map().mouseCoordinates() });
}
var end = osmNode({ loc: context.map().mouseCoordinates() });
// Push an annotated state for undo to return back to.
// We must make sure to remove this edit later.
context.perform(actionNoop(), annotation);
tempEdits++;
_tempEdits++;
// Add the temporary drawing entities to the graph.
// Add the drawing node to the graph.
// We must make sure to remove this edit later.
context.perform(AddDrawEntities());
tempEdits++;
context.perform(_actionAddDrawNode());
_tempEdits++;
// related code
// - `mode/drag_node.js` `doMode()`
// - `behavior/draw.js` `click()`
// - `behavior/draw_way.js` `move()`
function move(datum) {
var loc;
var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc;
var nodeGroups = datum && datum.properties && datum.properties.nodes;
var loc = context.map().mouseCoordinates();
if (datum.type === 'node' && datum.id !== end.id) {
loc = datum.loc;
if (nodeLoc) { // snap to node/vertex - a point target with `.loc`
loc = nodeLoc;
} else if (datum.type === 'way') {
var dims = context.map().dimensions(),
mouse = context.mouse(),
pad = 5,
trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
mouse[1] > pad && mouse[1] < dims[1] - pad;
if (trySnap) {
loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc;
} else if (nodeGroups) { // snap to way - a line target with `.nodes`
var best = Infinity;
for (var i = 0; i < nodeGroups.length; i++) {
var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); });
var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id);
if (choice && choice.distance < best) {
best = choice.distance;
loc = choice.loc;
}
}
}
if (!loc) {
loc = context.map().mouseCoordinates();
}
context.replace(actionMoveNode(end.id, loc));
end = context.entity(end.id);
// check if this movement causes the geometry to break
var doBlock = invalidGeometry(end, context.graph());
context.surface()
.classed('nope', doBlock);
}
function invalidGeometry(entity, graph) {
var parents = graph.parentWays(entity);
for (var i = 0; i < parents.length; i++) {
var parent = parents[i];
var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });
if (parent.isClosed()) {
if (geoHasSelfIntersections(nodes, entity.id)) {
return true;
}
}
}
return false;
}
@@ -99,7 +89,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
// Undo popped the history back to the initial annotated no-op edit.
// Remove initial no-op edit and whatever edit happened immediately before it.
context.pop(2);
tempEdits = 0;
_tempEdits = 0;
if (context.hasEntity(wayId)) {
context.enter(mode);
@@ -110,14 +100,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
function setActiveElements() {
var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id];
context.surface().selectAll(utilEntitySelector(active))
context.surface().selectAll('.' + end.id)
.classed('active', true);
}
var drawWay = function(surface) {
draw.on('move', move)
behavior
.on('move', move)
.on('click', drawWay.add)
.on('clickWay', drawWay.addWay)
.on('clickNode', drawWay.addNode)
@@ -131,7 +121,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
setActiveElements();
surface.call(draw);
surface.call(behavior);
context.history()
.on('undone.draw', undone);
@@ -142,8 +132,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
// Drawing was interrupted unexpectedly.
// This can happen if the user changes modes,
// clicks geolocate button, a hashchange event occurs, etc.
if (tempEdits) {
context.pop(tempEdits);
if (_tempEdits) {
context.pop(_tempEdits);
while (context.graph() !== startGraph) {
context.pop();
}
@@ -152,7 +142,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
context.map()
.on('drawn.draw', null);
surface.call(draw.off)
surface.call(behavior.off)
.selectAll('.active')
.classed('active', false);
@@ -161,129 +151,75 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
};
function AddDrawEntities() {
function _actionAddDrawNode() {
return function(graph) {
if (isArea) {
// For area drawing, there is no need for a temporary node.
// `end` gets inserted into the way as the penultimate node.
return graph
.replace(end)
.replace(origWay.addNode(end.id));
} else {
// For line drawing, add a temporary start, end, and segment to the graph.
// This allows us to class the new segment as `active`, but still
// connect it back to parts of the way that have already been drawn.
return graph
.replace(start)
.replace(end)
.replace(segment);
}
return graph
.replace(end)
.replace(origWay.addNode(end.id, index));
};
}
function ReplaceDrawEntities(newNode) {
function _actionReplaceDrawNode(newNode) {
return function(graph) {
if (isArea) {
// For area drawing, we didn't create a temporary node.
// `newNode` gets inserted into the _original_ way as the penultimate node.
return graph
.replace(origWay.addNode(newNode.id))
.remove(end);
} else {
// For line drawing, add the `newNode` to the way at specified index,
// and remove the temporary start, end, and segment.
return graph
.replace(origWay.addNode(newNode.id, index))
.remove(end)
.remove(segment)
.remove(start);
}
return graph
.replace(origWay.addNode(newNode.id, index))
.remove(end);
};
}
// Accept the current position of the temporary node and continue drawing.
drawWay.add = function(loc) {
// prevent duplicate nodes
var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]);
if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return;
context.pop(tempEdits);
if (isArea) {
context.perform(
AddDrawEntities(),
annotation
);
} else {
var newNode = osmNode({loc: loc});
context.perform(
actionAddEntity(newNode),
ReplaceDrawEntities(newNode),
annotation
);
// Accept the current position of the drawing node and continue drawing.
drawWay.add = function(loc, d) {
if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
return; // can't click here
}
tempEdits = 0;
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
_actionAddDrawNode(),
annotation
);
context.enter(mode);
};
// Connect the way to an existing way.
drawWay.addWay = function(loc, edge) {
if (isArea) {
context.pop(tempEdits);
context.perform(
AddDrawEntities(),
actionAddMidpoint({ loc: loc, edge: edge}, end),
annotation
);
} else {
var previousEdge = startIndex ?
[origWay.nodes[startIndex], origWay.nodes[startIndex - 1]] :
[origWay.nodes[0], origWay.nodes[1]];
// Avoid creating duplicate segments
if (geoEdgeEqual(edge, previousEdge))
return;
context.pop(tempEdits);
var newNode = osmNode({ loc: loc });
context.perform(
actionAddMidpoint({ loc: loc, edge: edge}, newNode),
ReplaceDrawEntities(newNode),
annotation
);
if (context.surface().classed('nope')) {
return; // can't click here
}
tempEdits = 0;
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
_actionAddDrawNode(),
actionAddMidpoint({ loc: loc, edge: edge }, end),
annotation
);
context.enter(mode);
};
// Connect the way to an existing node and continue drawing.
drawWay.addNode = function(node) {
// Avoid creating duplicate segments
if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return;
// Clicks should not occur on the drawing node, however a space keypress can
// sometimes grab that node's datum (before it gets classed as `active`?) #4016
if (node.id === end.id) {
drawWay.add(node.loc);
return;
if (context.surface().classed('nope')) {
return; // can't click here
}
context.pop(tempEdits);
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
ReplaceDrawEntities(node),
_actionReplaceDrawNode(node),
annotation
);
tempEdits = 0;
context.enter(mode);
};
@@ -292,8 +228,12 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
// If the way has enough nodes to be valid, it's selected.
// Otherwise, delete everything and return to browse mode.
drawWay.finish = function() {
context.pop(tempEdits);
tempEdits = 0;
if (context.surface().classed('nope')) {
return; // can't click here
}
context.pop(_tempEdits);
_tempEdits = 0;
var way = context.hasEntity(wayId);
if (!way || way.isDegenerate()) {
@@ -311,8 +251,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
// Cancel the draw operation, delete everything, and return to browse mode.
drawWay.cancel = function() {
context.pop(tempEdits);
tempEdits = 0;
context.pop(_tempEdits);
_tempEdits = 0;
while (context.graph() !== startGraph) {
context.pop();
@@ -322,12 +262,22 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
context.map().dblclickEnable(true);
}, 1000);
context.surface()
.classed('nope', false);
context.enter(modeBrowse(context));
};
drawWay.activeID = function() {
if (!arguments.length) return end.id;
// no assign
return drawWay;
};
drawWay.tail = function(text) {
draw.tail(text);
behavior.tail(text);
return drawWay;
};
+34 -29
View File
@@ -6,7 +6,7 @@ import {
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { osmEntity } from '../osm/index';
import { osmEntity } from '../osm';
import { utilRebind } from '../util/rebind';
@@ -20,16 +20,16 @@ import { utilRebind } from '../util/rebind';
have the .hover class.
*/
export function behaviorHover(context) {
var dispatch = d3_dispatch('hover'),
_selection = d3_select(null),
newId = null,
buttonDown,
altDisables,
target;
var dispatch = d3_dispatch('hover');
var _selection = d3_select(null);
var _newId = null;
var _buttonDown;
var _altDisables;
var _target;
function keydown() {
if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
_selection.selectAll('.hover')
.classed('hover-suppressed', true)
.classed('hover', false);
@@ -43,7 +43,7 @@ export function behaviorHover(context) {
function keyup() {
if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
_selection.selectAll('.hover-suppressed')
.classed('hover-suppressed', false)
.classed('hover', true);
@@ -51,14 +51,14 @@ export function behaviorHover(context) {
_selection
.classed('hover-disabled', false);
dispatch.call('hover', this, target ? target.id : null);
dispatch.call('hover', this, _target ? _target.id : null);
}
}
var hover = function(selection) {
_selection = selection;
newId = null;
_newId = null;
_selection
.on('mouseover.hover', mouseover)
@@ -71,65 +71,71 @@ export function behaviorHover(context) {
function mouseover() {
if (buttonDown) return;
if (_buttonDown) return;
var target = d3_event.target;
enter(target ? target.__data__ : null);
}
function mouseout() {
if (buttonDown) return;
if (_buttonDown) return;
var target = d3_event.relatedTarget;
enter(target ? target.__data__ : null);
}
function mousedown() {
buttonDown = true;
_buttonDown = true;
d3_select(window)
.on('mouseup.hover', mouseup, true);
}
function mouseup() {
buttonDown = false;
_buttonDown = false;
d3_select(window)
.on('mouseup.hover', null, true);
}
function enter(d) {
if (d === target) return;
target = d;
function enter(datum) {
if (datum === _target) return;
_target = datum;
_selection.selectAll('.hover')
.classed('hover', false);
_selection.selectAll('.hover-suppressed')
.classed('hover-suppressed', false);
if (target instanceof osmEntity && target.id !== newId) {
var entity;
if (datum instanceof osmEntity) {
entity = datum;
} else {
entity = datum && datum.properties && datum.properties.entity;
}
if (entity && entity.id !== _newId) {
// If drawing a way, don't hover on a node that was just placed. #3974
var mode = context.mode() && context.mode().id;
if ((mode === 'draw-line' || mode === 'draw-area') && !newId && target.type === 'node') {
newId = target.id;
if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') {
_newId = entity.id;
return;
}
var selector = '.' + target.id;
var selector = '.' + entity.id;
if (target.type === 'relation') {
target.members.forEach(function(member) {
if (entity.type === 'relation') {
entity.members.forEach(function(member) {
selector += ', .' + member.id;
});
}
var suppressed = altDisables && d3_event && d3_event.altKey;
var suppressed = _altDisables && d3_event && d3_event.altKey;
_selection.selectAll(selector)
.classed(suppressed ? 'hover-suppressed' : 'hover', true);
dispatch.call('hover', this, !suppressed && target.id);
dispatch.call('hover', this, !suppressed && entity.id);
} else {
dispatch.call('hover', this, null);
@@ -147,7 +153,6 @@ export function behaviorHover(context) {
selection
.classed('hover-disabled', false);
selection
.on('mouseover.hover', null)
.on('mouseout.hover', null)
@@ -160,8 +165,8 @@ export function behaviorHover(context) {
hover.altDisables = function(_) {
if (!arguments.length) return altDisables;
altDisables = _;
if (!arguments.length) return _altDisables;
_altDisables = _;
return hover;
};
+13 -11
View File
@@ -6,7 +6,7 @@ import {
select as d3_select
} from 'd3-selection';
import { geoEuclideanDistance } from '../geo';
import { geoVecLength } from '../geo';
import {
modeBrowse,
@@ -17,10 +17,10 @@ import { osmEntity } from '../osm';
export function behaviorSelect(context) {
var lastMouse = null,
suppressMenu = true,
tolerance = 4,
p1 = null;
var lastMouse = null;
var suppressMenu = true;
var tolerance = 4;
var p1 = null;
function point() {
@@ -102,19 +102,21 @@ export function behaviorSelect(context) {
.on('mouseup.select', null, true);
if (!p1) return;
var p2 = point(),
dist = geoEuclideanDistance(p1, p2);
var p2 = point();
var dist = geoVecLength(p1, p2);
p1 = null;
if (dist > tolerance) {
return;
}
var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(),
isShowAlways = +context.storage('edit-menu-show-always') === 1,
datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__),
mode = context.mode();
var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node();
var isShowAlways = +context.storage('edit-menu-show-always') === 1;
var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__);
var mode = context.mode();
var entity = datum && datum.properties && datum.properties.entity;
if (entity) datum = entity;
if (datum && datum.type === 'midpoint') {
datum = datum.parents[0];
+9 -5
View File
@@ -255,6 +255,9 @@ export function coreContext() {
return [];
}
};
context.activeID = function() {
return mode && mode.activeID && mode.activeID();
};
/* Behaviors */
@@ -310,11 +313,12 @@ export function coreContext() {
/* Debug */
var debugFlags = {
tile: false,
collision: false,
imagery: false,
imperial: false,
driveLeft: false
tile: false, // tile boundaries
collision: false, // label collision bounding boxes
imagery: false, // imagery bounding polygons
imperial: false, // imperial (not metric) bounding polygons
driveLeft: false, // driveLeft bounding polygons
target: false // touch targets
};
context.debugFlags = function() {
return debugFlags;
+27 -234
View File
@@ -1,277 +1,70 @@
import _every from 'lodash-es/every';
import _some from 'lodash-es/some';
// constants
var TAU = 2 * Math.PI;
var EQUATORIAL_RADIUS = 6356752.314245179;
var POLAR_RADIUS = 6378137.0;
export function geoRoundCoords(c) {
return [Math.floor(c[0]), Math.floor(c[1])];
}
export function geoInterp(p1, p2, t) {
return [p1[0] + (p2[0] - p1[0]) * t,
p1[1] + (p2[1] - p1[1]) * t];
}
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
// Returns a positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are collinear.
export function geoCross(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
// http://jsperf.com/id-dist-optimization
export function geoEuclideanDistance(a, b) {
var x = a[0] - b[0], y = a[1] - b[1];
return Math.sqrt((x * x) + (y * y));
}
// using WGS84 polar radius (6356752.314245179 m)
// const = 2 * PI * r / 360
export function geoLatToMeters(dLat) {
return dLat * 110946.257617;
return dLat * (TAU * POLAR_RADIUS / 360);
}
// using WGS84 equatorial radius (6378137.0 m)
// const = 2 * PI * r / 360
export function geoLonToMeters(dLon, atLat) {
return Math.abs(atLat) >= 90 ? 0 :
dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180)));
dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180)));
}
// using WGS84 polar radius (6356752.314245179 m)
// const = 2 * PI * r / 360
export function geoMetersToLat(m) {
return m / 110946.257617;
return m / (TAU * POLAR_RADIUS / 360);
}
// using WGS84 equatorial radius (6378137.0 m)
// const = 2 * PI * r / 360
export function geoMetersToLon(m, atLat) {
return Math.abs(atLat) >= 90 ? 0 :
m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180)));
m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180)));
}
export function geoOffsetToMeters(offset) {
var equatRadius = 6356752.314245179,
polarRadius = 6378137.0,
tileSize = 256;
export function geoMetersToOffset(meters, tileSize) {
tileSize = tileSize || 256;
return [
offset[0] * 2 * Math.PI * equatRadius / tileSize,
-offset[1] * 2 * Math.PI * polarRadius / tileSize
meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS),
-meters[1] * tileSize / (TAU * POLAR_RADIUS)
];
}
export function geoMetersToOffset(meters) {
var equatRadius = 6356752.314245179,
polarRadius = 6378137.0,
tileSize = 256;
export function geoOffsetToMeters(offset, tileSize) {
tileSize = tileSize || 256;
return [
meters[0] * tileSize / (2 * Math.PI * equatRadius),
-meters[1] * tileSize / (2 * Math.PI * polarRadius)
offset[0] * TAU * EQUATORIAL_RADIUS / tileSize,
-offset[1] * TAU * POLAR_RADIUS / tileSize
];
}
// Equirectangular approximation of spherical distances on Earth
export function geoSphericalDistance(a, b) {
var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2),
y = geoLatToMeters(a[1] - b[1]);
var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2);
var y = geoLatToMeters(a[1] - b[1]);
return Math.sqrt((x * x) + (y * y));
}
export function geoEdgeEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] === b[1] && a[1] === b[0]);
// scale to zoom
export function geoScaleToZoom(k, tileSize) {
tileSize = tileSize || 256;
var log2ts = Math.log(tileSize) * Math.LOG2E;
return Math.log(k * TAU) / Math.LN2 - log2ts;
}
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
export function geoAngle(a, b, projection) {
a = projection(a.loc);
b = projection(b.loc);
return Math.atan2(b[1] - a[1], b[0] - a[0]);
// zoom to scale
export function geoZoomToScale(z, tileSize) {
tileSize = tileSize || 256;
return tileSize * Math.pow(2, z) / TAU;
}
// Rotate all points counterclockwise around a pivot point by given angle
export function geoRotate(points, angle, around) {
return points.map(function(point) {
var radial = [point[0] - around[0], point[1] - around[1]];
return [
radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0],
radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1]
];
});
}
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
export function geoChooseEdge(nodes, point, projection) {
var dist = geoEuclideanDistance,
points = nodes.map(function(n) { return projection(n.loc); }),
min = Infinity,
idx, loc;
function dot(p, q) {
return p[0] * q[0] + p[1] * q[1];
}
for (var i = 0; i < points.length - 1; i++) {
var o = points[i],
s = [points[i + 1][0] - o[0],
points[i + 1][1] - o[1]],
v = [point[0] - o[0],
point[1] - o[1]],
proj = dot(v, s) / dot(s, s),
p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
return {
index: idx,
distance: min,
loc: loc
};
}
// Return the intersection point of 2 line segments.
// From https://github.com/pgkelley4/line-segments-intersect
// This uses the vector cross product approach described below:
// http://stackoverflow.com/a/565282/786339
export function geoLineIntersection(a, b) {
function subtractPoints(point1, point2) {
return [point1[0] - point2[0], point1[1] - point2[1]];
}
function crossProduct(point1, point2) {
return point1[0] * point2[1] - point1[1] * point2[0];
}
var p = [a[0][0], a[0][1]],
p2 = [a[1][0], a[1][1]],
q = [b[0][0], b[0][1]],
q2 = [b[1][0], b[1][1]],
r = subtractPoints(p2, p),
s = subtractPoints(q2, q),
uNumerator = crossProduct(subtractPoints(q, p), r),
denominator = crossProduct(r, s);
if (uNumerator && denominator) {
var u = uNumerator / denominator,
t = crossProduct(subtractPoints(q, p), s) / denominator;
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
return geoInterp(p, p2, t);
}
}
return null;
}
export function geoPathIntersections(path1, path2) {
var intersections = [];
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ],
b = [ path2[j], path2[j+1] ],
hit = geoLineIntersection(a, b);
if (hit) intersections.push(hit);
}
}
return intersections;
}
// Return whether point is contained in polygon.
//
// `point` should be a 2-item array of coordinates.
// `polygon` should be an array of 2-item arrays of coordinates.
//
// From https://github.com/substack/point-in-polygon.
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
//
export function geoPointInPolygon(point, polygon) {
var x = point[0],
y = point[1],
inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1];
var xj = polygon[j][0], yj = polygon[j][1];
var intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
export function geoPolygonContainsPolygon(outer, inner) {
return _every(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) {
function testSegments(outer, inner) {
for (var i = 0; i < outer.length - 1; i++) {
for (var j = 0; j < inner.length - 1; j++) {
var a = [ outer[i], outer[i+1] ],
b = [ inner[j], inner[j+1] ];
if (geoLineIntersection(a, b)) return true;
}
}
return false;
}
function testPoints(outer, inner) {
return _some(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner));
}
export function geoPathLength(path) {
var length = 0;
for (var i = 0; i < path.length - 1; i++) {
length += geoEuclideanDistance(path[i], path[i + 1]);
}
return length;
}
+252
View File
@@ -0,0 +1,252 @@
import _every from 'lodash-es/every';
import _some from 'lodash-es/some';
import {
geoVecAngle,
geoVecCross,
geoVecDot,
geoVecEqual,
geoVecInterp,
geoVecLength,
geoVecSubtract
} from './vector.js';
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
export function geoAngle(a, b, projection) {
return geoVecAngle(projection(a.loc), projection(b.loc));
}
export function geoEdgeEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] === b[1] && a[1] === b[0]);
}
// Rotate all points counterclockwise around a pivot point by given angle
export function geoRotate(points, angle, around) {
return points.map(function(point) {
var radial = geoVecSubtract(point, around);
return [
radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0],
radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1]
];
});
}
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
export function geoChooseEdge(nodes, point, projection, activeID) {
var dist = geoVecLength;
var points = nodes.map(function(n) { return projection(n.loc); });
var ids = nodes.map(function(n) { return n.id; });
var min = Infinity;
var idx;
var loc;
for (var i = 0; i < points.length - 1; i++) {
if (ids[i] === activeID || ids[i + 1] === activeID) continue;
var o = points[i];
var s = geoVecSubtract(points[i + 1], o);
var v = geoVecSubtract(point, o);
var proj = geoVecDot(v, s) / geoVecDot(s, s);
var p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
if (idx !== undefined) {
return { index: idx, distance: min, loc: loc };
} else {
return null;
}
}
// check active (dragged or drawing) segments against inactive segments
export function geoHasSelfIntersections(nodes, activeID) {
var actives = [];
var inactives = [];
var j, k;
for (j = 0; j < nodes.length - 1; j++) {
var n1 = nodes[j];
var n2 = nodes[j+1];
var segment = [n1.loc, n2.loc];
if (n1.id === activeID || n2.id === activeID) {
actives.push(segment);
} else {
inactives.push(segment);
}
}
for (j = 0; j < actives.length; j++) {
for (k = 0; k < inactives.length; k++) {
var p = actives[j];
var q = inactives[k];
// skip if segments share an endpoint
if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) ||
geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) {
continue;
} else if (geoLineIntersection(p, q)) {
return true;
}
}
}
return false;
}
// Return the intersection point of 2 line segments.
// From https://github.com/pgkelley4/line-segments-intersect
// This uses the vector cross product approach described below:
// http://stackoverflow.com/a/565282/786339
export function geoLineIntersection(a, b) {
var p = [a[0][0], a[0][1]];
var p2 = [a[1][0], a[1][1]];
var q = [b[0][0], b[0][1]];
var q2 = [b[1][0], b[1][1]];
var r = geoVecSubtract(p2, p);
var s = geoVecSubtract(q2, q);
var uNumerator = geoVecCross(geoVecSubtract(q, p), r);
var denominator = geoVecCross(r, s);
if (uNumerator && denominator) {
var u = uNumerator / denominator;
var t = geoVecCross(geoVecSubtract(q, p), s) / denominator;
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
return geoVecInterp(p, p2, t);
}
}
return null;
}
export function geoPathIntersections(path1, path2) {
var intersections = [];
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ];
var b = [ path2[j], path2[j+1] ];
var hit = geoLineIntersection(a, b);
if (hit) {
intersections.push(hit);
}
}
}
return intersections;
}
export function geoPathHasIntersections(path1, path2) {
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ];
var b = [ path2[j], path2[j+1] ];
var hit = geoLineIntersection(a, b);
if (hit) {
return true;
}
}
}
return false;
}
// Return whether point is contained in polygon.
//
// `point` should be a 2-item array of coordinates.
// `polygon` should be an array of 2-item arrays of coordinates.
//
// From https://github.com/substack/point-in-polygon.
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
//
export function geoPointInPolygon(point, polygon) {
var x = point[0];
var y = point[1];
var inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0];
var yi = polygon[i][1];
var xj = polygon[j][0];
var yj = polygon[j][1];
var intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
export function geoPolygonContainsPolygon(outer, inner) {
return _every(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) {
function testPoints(outer, inner) {
return _some(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
return testPoints(outer, inner) || (!!checkSegments && geoPathHasIntersections(outer, inner));
}
export function geoPathLength(path) {
var length = 0;
for (var i = 0; i < path.length - 1; i++) {
length += geoVecLength(path[i], path[i + 1]);
}
return length;
}
// If the given point is at the edge of the padded viewport,
// return a vector that will nudge the viewport in that direction
export function geoViewportEdge(point, dimensions) {
var pad = [80, 20, 50, 20]; // top, right, bottom, left
var x = 0;
var y = 0;
if (point[0] > dimensions[0] - pad[1])
x = -10;
if (point[0] < pad[3])
x = 10;
if (point[1] > dimensions[1] - pad[2])
y = -10;
if (point[1] < pad[0])
y = 10;
if (x || y) {
return [x, y];
} else {
return null;
}
}
+31 -15
View File
@@ -1,23 +1,39 @@
export { geoAngle } from './geo.js';
export { geoChooseEdge } from './geo.js';
export { geoCross } from './geo.js';
export { geoEdgeEqual } from './geo.js';
export { geoEuclideanDistance } from './geo.js';
export { geoExtent } from './extent.js';
export { geoInterp } from './geo.js';
export { geoRawMercator } from './raw_mercator.js';
export { geoRoundCoords } from './geo.js';
export { geoRotate } from './geo.js';
export { geoLatToMeters } from './geo.js';
export { geoLineIntersection } from './geo.js';
export { geoLonToMeters } from './geo.js';
export { geoMetersToLat } from './geo.js';
export { geoMetersToLon } from './geo.js';
export { geoMetersToOffset } from './geo.js';
export { geoOffsetToMeters } from './geo.js';
export { geoPathIntersections } from './geo.js';
export { geoPathLength } from './geo.js';
export { geoPointInPolygon } from './geo.js';
export { geoPolygonContainsPolygon } from './geo.js';
export { geoPolygonIntersectsPolygon } from './geo.js';
export { geoScaleToZoom } from './geo.js';
export { geoSphericalDistance } from './geo.js';
export { geoZoomToScale } from './geo.js';
export { geoAngle } from './geom.js';
export { geoChooseEdge } from './geom.js';
export { geoEdgeEqual } from './geom.js';
export { geoHasSelfIntersections } from './geom.js';
export { geoRotate } from './geom.js';
export { geoLineIntersection } from './geom.js';
export { geoPathHasIntersections } from './geom.js';
export { geoPathIntersections } from './geom.js';
export { geoPathLength } from './geom.js';
export { geoPointInPolygon } from './geom.js';
export { geoPolygonContainsPolygon } from './geom.js';
export { geoPolygonIntersectsPolygon } from './geom.js';
export { geoViewportEdge } from './geom.js';
export { geoRawMercator } from './raw_mercator.js';
export { geoVecAdd } from './vector.js';
export { geoVecAngle } from './vector.js';
export { geoVecCross } from './vector.js';
export { geoVecDot } from './vector.js';
export { geoVecEqual } from './vector.js';
export { geoVecFloor } from './vector.js';
export { geoVecInterp } from './vector.js';
export { geoVecLength } from './vector.js';
export { geoVecSubtract } from './vector.js';
export { geoVecScale } from './vector.js';
+62
View File
@@ -0,0 +1,62 @@
// vector equals
export function geoVecEqual(a, b) {
return (a[0] === b[0]) && (a[1] === b[1]);
}
// vector addition
export function geoVecAdd(a, b) {
return [ a[0] + b[0], a[1] + b[1] ];
}
// vector subtraction
export function geoVecSubtract(a, b) {
return [ a[0] - b[0], a[1] - b[1] ];
}
// vector multiplication
export function geoVecScale(a, b) {
return [ a[0] * b, a[1] * b ];
}
// vector rounding (was: geoRoundCoordinates)
export function geoVecFloor(a) {
return [ Math.floor(a[0]), Math.floor(a[1]) ];
}
// linear interpolation
export function geoVecInterp(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t
];
}
// http://jsperf.com/id-dist-optimization
export function geoVecLength(a, b) {
var x = a[0] - b[0];
var y = a[1] - b[1];
return Math.sqrt((x * x) + (y * y));
}
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
export function geoVecAngle(a, b) {
return Math.atan2(b[1] - a[1], b[0] - a[0]);
}
// dot product
export function geoVecDot(a, b, origin) {
origin = origin || [0, 0];
return (a[0] - origin[0]) * (b[0] - origin[0]) +
(a[1] - origin[1]) * (b[1] - origin[1]);
}
// 2D cross product of OA and OB vectors, returns magnitude of Z vector
// Returns a positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are collinear.
export function geoVecCross(a, b, origin) {
origin = origin || [0, 0];
return (a[0] - origin[0]) * (b[1] - origin[1]) -
(a[1] - origin[1]) * (b[0] - origin[0]);
}
+4
View File
@@ -29,6 +29,10 @@ export { coreDifference as Difference } from './core/difference';
export { coreGraph as Graph } from './core/graph';
export { coreHistory as History } from './core/history';
export { coreTree as Tree } from './core/tree';
export { geoVecCross as geoCross } from './geo/vector';
export { geoVecInterp as geoInterp } from './geo/vector';
export { geoVecFloor as geoRoundCoordinates } from './geo/vector';
export { geoVecLength as geoEuclideanDistance } from './geo/vector';
export { osmEntity as Entity } from './osm/entity';
export { osmNode as Node } from './osm/node';
export { osmRelation as Relation } from './osm/relation';
+191 -100
View File
@@ -1,4 +1,4 @@
import _map from 'lodash-es/map';
import _find from 'lodash-es/find';
import {
event as d3_event,
@@ -21,13 +21,15 @@ import {
} from '../behavior';
import {
modeBrowse,
modeSelect
} from './index';
geoChooseEdge,
geoHasSelfIntersections,
geoPathHasIntersections,
geoVecSubtract,
geoViewportEdge
} from '../geo';
import { geoChooseEdge } from '../geo';
import { osmNode } from '../osm';
import { utilEntitySelector } from '../util';
import { modeBrowse, modeSelect } from './index';
import { osmJoinWays, osmNode } from '../osm';
import { uiFlash } from '../ui';
@@ -36,46 +38,22 @@ export function modeDragNode(context) {
id: 'drag-node',
button: 'browse'
};
var hover = behaviorHover(context).altDisables(true)
.on('hover', context.ui().sidebar.hover);
var edit = behaviorEdit(context);
var nudgeInterval,
activeIDs,
wasMidpoint,
isCancelled,
lastLoc,
selectedIDs = [],
hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover),
edit = behaviorEdit(context);
function vecSub(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
function edge(point, size) {
var pad = [80, 20, 50, 20], // top, right, bottom, left
x = 0,
y = 0;
if (point[0] > size[0] - pad[1])
x = -10;
if (point[0] < pad[3])
x = 10;
if (point[1] > size[1] - pad[2])
y = -10;
if (point[1] < pad[0])
y = 10;
if (x || y) {
return [x, y];
} else {
return null;
}
}
var _nudgeInterval;
var _restoreSelectedIDs = [];
var _wasMidpoint = false;
var _isCancelled = false;
var _activeEntity;
var _startLoc;
var _lastLoc;
function startNudge(entity, nudge) {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = window.setInterval(function() {
if (_nudgeInterval) window.clearInterval(_nudgeInterval);
_nudgeInterval = window.setInterval(function() {
context.pan(nudge);
doMove(entity, nudge);
}, 50);
@@ -83,9 +61,9 @@ export function modeDragNode(context) {
function stopNudge() {
if (nudgeInterval) {
window.clearInterval(nudgeInterval);
nudgeInterval = null;
if (_nudgeInterval) {
window.clearInterval(_nudgeInterval);
_nudgeInterval = null;
}
}
@@ -106,46 +84,52 @@ export function modeDragNode(context) {
function start(entity) {
wasMidpoint = entity.type === 'midpoint';
_wasMidpoint = entity.type === 'midpoint';
var hasHidden = context.features().hasHiddenConnections(entity, context.graph());
isCancelled = d3_event.sourceEvent.shiftKey || hasHidden;
_isCancelled = d3_event.sourceEvent.shiftKey || hasHidden;
if (isCancelled) {
if (_isCancelled) {
if (hasHidden) {
uiFlash().text(t('modes.drag_node.connected_to_hidden'))();
}
return behavior.cancel();
return drag.cancel();
}
if (wasMidpoint) {
if (_wasMidpoint) {
var midpoint = entity;
entity = osmNode();
context.perform(actionAddMidpoint(midpoint, entity));
entity = context.entity(entity.id); // get post-action entity
var vertex = context.surface().selectAll('.' + entity.id);
behavior.target(vertex.node(), entity);
drag.target(vertex.node(), entity);
} else {
context.perform(actionNoop());
}
// activeIDs generate no pointer events. This prevents the node or vertex
// being dragged from trying to connect to itself or its parent element.
activeIDs = _map(context.graph().parentWays(entity), 'id');
activeIDs.push(entity.id);
setActiveElements();
_activeEntity = entity;
_startLoc = entity.loc;
context.surface().selectAll('.' + _activeEntity.id)
.classed('active', true);
context.enter(mode);
}
// related code
// - `behavior/draw.js` `datum()`
function datum() {
var event = d3_event && d3_event.sourceEvent;
if (!event || event.altKey) {
return {};
} else {
return event.target.__data__ || {};
// When dragging, snap only to touch targets..
// (this excludes area fills and active drawing elements)
var d = event.target.__data__;
return (d && d.properties && d.properties.target) ? d : {};
}
}
@@ -153,16 +137,32 @@ export function modeDragNode(context) {
function doMove(entity, nudge) {
nudge = nudge || [0, 0];
var currPoint = (d3_event && d3_event.point) || context.projection(lastLoc),
currMouse = vecSub(currPoint, nudge),
loc = context.projection.invert(currMouse),
d = datum();
var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);
var currMouse = geoVecSubtract(currPoint, nudge);
var loc = context.projection.invert(currMouse);
if (!nudgeInterval) {
if (d.type === 'node' && d.id !== entity.id) {
loc = d.loc;
} else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) {
loc = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection).loc;
if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap..
// related code
// - `mode/drag_node.js` `doMode()`
// - `behavior/draw.js` `click()`
// - `behavior/draw_way.js` `move()`
var d = datum();
var nodeLoc = d && d.properties && d.properties.entity && d.properties.entity.loc;
var nodeGroups = d && d.properties && d.properties.nodes;
if (nodeLoc) { // snap to node/vertex - a point target with `.loc`
loc = nodeLoc;
} else if (nodeGroups) { // snap to way - a line target with `.nodes`
var best = Infinity;
for (var i = 0; i < nodeGroups.length; i++) {
var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); });
var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id);
if (choice && choice.distance < best) {
best = choice.distance;
loc = choice.loc;
}
}
}
}
@@ -171,17 +171,79 @@ export function modeDragNode(context) {
moveAnnotation(entity)
);
lastLoc = loc;
// check if this movement causes the geometry to break
var doBlock = invalidGeometry(entity, context.graph());
context.surface()
.classed('nope', doBlock);
_lastLoc = loc;
}
function invalidGeometry(entity, graph) {
var parents = graph.parentWays(entity);
var i, j, k;
for (i = 0; i < parents.length; i++) {
var parent = parents[i];
var nodes = [];
var activeIndex = null; // which multipolygon ring contains node being dragged
// test any parent multipolygons for valid geometry
var relations = graph.parentRelations(parent);
for (j = 0; j < relations.length; j++) {
if (!relations[j].isMultipolygon()) continue;
var rings = osmJoinWays(relations[j].members, graph);
// find active ring and test it for self intersections
for (k = 0; k < rings.length; k++) {
nodes = rings[k].nodes;
if (_find(nodes, function(n) { return n.id === entity.id; })) {
activeIndex = k;
if (geoHasSelfIntersections(nodes, entity.id)) {
return true;
}
}
rings[k].coords = nodes.map(function(n) { return n.loc; });
}
// test active ring for intersections with other rings in the multipolygon
for (k = 0; k < rings.length; k++) {
if (k === activeIndex) continue;
// make sure active ring doesnt cross passive rings
if (geoPathHasIntersections(rings[activeIndex].coords, rings[k].coords)) {
return true;
}
}
}
// If we still haven't tested this node's parent way for self-intersections.
// (because it's not a member of a multipolygon), test it now.
if (activeIndex !== null && parent.isClosed()) {
nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });
if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) {
return true;
}
}
}
return false;
}
function move(entity) {
if (isCancelled) return;
if (_isCancelled) return;
d3_event.sourceEvent.stopPropagation();
lastLoc = context.projection.invert(d3_event.point);
_lastLoc = context.projection.invert(d3_event.point);
doMove(entity);
var nudge = edge(d3_event.point, context.map().dimensions());
var nudge = geoViewportEdge(d3_event.point, context.map().dimensions());
if (nudge) {
startNudge(entity, nudge);
} else {
@@ -191,24 +253,34 @@ export function modeDragNode(context) {
function end(entity) {
if (isCancelled) return;
if (_isCancelled) return;
var d = datum();
var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope');
var target = d && d.properties && d.properties.entity; // entity to snap to
if (d.type === 'way') {
var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection);
context.replace(
actionAddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity),
connectAnnotation(d)
if (nope) { // bounce back
context.perform(
_actionBounceBack(entity.id, _startLoc)
);
} else if (d.type === 'node' && d.id !== entity.id) {
} else if (target && target.type === 'way') {
var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id);
context.replace(
actionConnect([d.id, entity.id]),
connectAnnotation(d)
actionAddMidpoint({
loc: choice.loc,
edge: [target.nodes[choice.index - 1], target.nodes[choice.index]]
}, entity),
connectAnnotation(target)
);
} else if (wasMidpoint) {
} else if (target && target.type === 'node') {
context.replace(
actionConnect([target.id, entity.id]),
connectAnnotation(target)
);
} else if (_wasMidpoint) {
context.replace(
actionNoop(),
t('operations.add.annotation.vertex')
@@ -221,7 +293,7 @@ export function modeDragNode(context) {
);
}
var reselection = selectedIDs.filter(function(id) {
var reselection = _restoreSelectedIDs.filter(function(id) {
return context.graph().hasEntity(id);
});
@@ -233,20 +305,27 @@ export function modeDragNode(context) {
}
function _actionBounceBack(nodeID, toLoc) {
var moveNode = actionMoveNode(nodeID, toLoc);
var action = function(graph, t) {
// last time through, pop off the bounceback perform.
// it will then overwrite the initial perform with a moveNode that does nothing
if (t === 1) context.pop();
return moveNode(graph, t);
};
action.transitionable = true;
return action;
}
function cancel() {
behavior.cancel();
drag.cancel();
context.enter(modeBrowse(context));
}
function setActiveElements() {
context.surface().selectAll(utilEntitySelector(activeIDs))
.classed('active', true);
}
var behavior = behaviorDrag()
.selector('g.node, g.point, g.midpoint')
var drag = behaviorDrag()
.selector('.layer-points-targets .target')
.surface(d3_select('#map').node())
.origin(origin)
.on('start', start)
@@ -260,11 +339,6 @@ export function modeDragNode(context) {
context.history()
.on('undone.drag-node', cancel);
context.map()
.on('drawn.drag-node', setActiveElements);
setActiveElements();
};
@@ -279,7 +353,10 @@ export function modeDragNode(context) {
context.map()
.on('drawn.drag-node', null);
_activeEntity = null;
context.surface()
.classed('nope', false)
.selectAll('.active')
.classed('active', false);
@@ -287,14 +364,28 @@ export function modeDragNode(context) {
};
mode.selectedIDs = function(_) {
if (!arguments.length) return selectedIDs;
selectedIDs = _;
mode.selectedIDs = function() {
if (!arguments.length) return _activeEntity ? [_activeEntity.id] : [];
// no assign
return mode;
};
mode.behavior = behavior;
mode.activeID = function() {
if (!arguments.length) return _activeEntity && _activeEntity.id;
// no assign
return mode;
};
mode.restoreSelectedIDs = function(_) {
if (!arguments.length) return _restoreSelectedIDs;
_restoreSelectedIDs = _;
return mode;
};
mode.behavior = drag;
return mode;
+7 -2
View File
@@ -20,8 +20,8 @@ export function modeDrawArea(context, wayId, startGraph) {
var addNode = behavior.addNode;
behavior.addNode = function(node) {
var length = way.nodes.length,
penultimate = length > 2 ? way.nodes[length - 2] : null;
var length = way.nodes.length;
var penultimate = length > 2 ? way.nodes[length - 2] : null;
if (node.id === way.first() || node.id === penultimate) {
behavior.finish();
@@ -44,5 +44,10 @@ export function modeDrawArea(context, wayId, startGraph) {
};
mode.activeID = function() {
return (behavior && behavior.activeID()) || [];
};
return mode;
}
+7 -4
View File
@@ -12,15 +12,14 @@ export function modeDrawLine(context, wayId, startGraph, affix) {
mode.enter = function() {
var way = context.entity(wayId),
index = (affix === 'prefix') ? 0 : undefined,
headId = (affix === 'prefix') ? way.first() : way.last();
var way = context.entity(wayId);
var index = (affix === 'prefix') ? 0 : undefined;
var headId = (affix === 'prefix') ? way.first() : way.last();
behavior = behaviorDrawWay(context, wayId, index, mode, startGraph)
.tail(t('modes.draw_line.tail'));
var addNode = behavior.addNode;
behavior.addNode = function(node) {
if (node.id === headId) {
behavior.finish();
@@ -43,5 +42,9 @@ export function modeDrawLine(context, wayId, startGraph, affix) {
};
mode.activeID = function() {
return (behavior && behavior.activeID()) || [];
};
return mode;
}
+43 -56
View File
@@ -8,6 +8,7 @@ import { t } from '../util/locale';
import { actionMove } from '../actions';
import { behaviorEdit } from '../behavior';
import { geoViewportEdge } from '../geo';
import {
modeBrowse,
@@ -30,23 +31,24 @@ export function modeMove(context, entityIDs, baseGraph) {
button: 'browse'
};
var keybinding = d3_keybinding('move'),
behaviors = [
behaviorEdit(context),
operationCircularize(entityIDs, context).behavior,
operationDelete(entityIDs, context).behavior,
operationOrthogonalize(entityIDs, context).behavior,
operationReflectLong(entityIDs, context).behavior,
operationReflectShort(entityIDs, context).behavior,
operationRotate(entityIDs, context).behavior
],
annotation = entityIDs.length === 1 ?
t('operations.move.annotation.' + context.geometry(entityIDs[0])) :
t('operations.move.annotation.multiple'),
prevGraph,
cache,
origin,
nudgeInterval;
var keybinding = d3_keybinding('move');
var behaviors = [
behaviorEdit(context),
operationCircularize(entityIDs, context).behavior,
operationDelete(entityIDs, context).behavior,
operationOrthogonalize(entityIDs, context).behavior,
operationReflectLong(entityIDs, context).behavior,
operationReflectShort(entityIDs, context).behavior,
operationRotate(entityIDs, context).behavior
];
var annotation = entityIDs.length === 1 ?
t('operations.move.annotation.' + context.geometry(entityIDs[0])) :
t('operations.move.annotation.multiple');
var _prevGraph;
var _cache;
var _origin;
var _nudgeInterval;
function vecSub(a, b) {
@@ -54,52 +56,30 @@ export function modeMove(context, entityIDs, baseGraph) {
}
function edge(point, size) {
var pad = [80, 20, 50, 20], // top, right, bottom, left
x = 0,
y = 0;
if (point[0] > size[0] - pad[1])
x = -10;
if (point[0] < pad[3])
x = 10;
if (point[1] > size[1] - pad[2])
y = -10;
if (point[1] < pad[0])
y = 10;
if (x || y) {
return [x, y];
} else {
return null;
}
}
function doMove(nudge) {
nudge = nudge || [0, 0];
var fn;
if (prevGraph !== context.graph()) {
cache = {};
origin = context.map().mouseCoordinates();
if (_prevGraph !== context.graph()) {
_cache = {};
_origin = context.map().mouseCoordinates();
fn = context.perform;
} else {
fn = context.overwrite;
}
var currMouse = context.mouse(),
origMouse = context.projection(origin),
delta = vecSub(vecSub(currMouse, origMouse), nudge);
var currMouse = context.mouse();
var origMouse = context.projection(_origin);
var delta = vecSub(vecSub(currMouse, origMouse), nudge);
fn(actionMove(entityIDs, delta, context.projection, cache), annotation);
prevGraph = context.graph();
fn(actionMove(entityIDs, delta, context.projection, _cache), annotation);
_prevGraph = context.graph();
}
function startNudge(nudge) {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = window.setInterval(function() {
if (_nudgeInterval) window.clearInterval(_nudgeInterval);
_nudgeInterval = window.setInterval(function() {
context.pan(nudge);
doMove(nudge);
}, 50);
@@ -107,16 +87,16 @@ export function modeMove(context, entityIDs, baseGraph) {
function stopNudge() {
if (nudgeInterval) {
window.clearInterval(nudgeInterval);
nudgeInterval = null;
if (_nudgeInterval) {
window.clearInterval(_nudgeInterval);
_nudgeInterval = null;
}
}
function move() {
doMove();
var nudge = edge(context.mouse(), context.map().dimensions());
var nudge = geoViewportEdge(context.mouse(), context.map().dimensions());
if (nudge) {
startNudge(nudge);
} else {
@@ -150,9 +130,9 @@ export function modeMove(context, entityIDs, baseGraph) {
mode.enter = function() {
origin = context.map().mouseCoordinates();
prevGraph = null;
cache = {};
_origin = context.map().mouseCoordinates();
_prevGraph = null;
_cache = {};
behaviors.forEach(function(behavior) {
context.install(behavior);
@@ -192,5 +172,12 @@ export function modeMove(context, entityIDs, baseGraph) {
};
mode.selectedIDs = function() {
if (!arguments.length) return entityIDs;
// no assign
return mode;
};
return mode;
}
+46 -38
View File
@@ -13,7 +13,7 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { t } from '../util/locale';
import { actionRotate } from '../actions';
import { behaviorEdit } from '../behavior';
import { geoInterp } from '../geo';
import { geoVecInterp } from '../geo';
import {
modeBrowse,
@@ -38,66 +38,67 @@ export function modeRotate(context, entityIDs) {
button: 'browse'
};
var keybinding = d3_keybinding('rotate'),
behaviors = [
behaviorEdit(context),
operationCircularize(entityIDs, context).behavior,
operationDelete(entityIDs, context).behavior,
operationMove(entityIDs, context).behavior,
operationOrthogonalize(entityIDs, context).behavior,
operationReflectLong(entityIDs, context).behavior,
operationReflectShort(entityIDs, context).behavior
],
annotation = entityIDs.length === 1 ?
t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) :
t('operations.rotate.annotation.multiple'),
prevGraph,
prevAngle,
prevTransform,
pivot;
var keybinding = d3_keybinding('rotate');
var behaviors = [
behaviorEdit(context),
operationCircularize(entityIDs, context).behavior,
operationDelete(entityIDs, context).behavior,
operationMove(entityIDs, context).behavior,
operationOrthogonalize(entityIDs, context).behavior,
operationReflectLong(entityIDs, context).behavior,
operationReflectShort(entityIDs, context).behavior
];
var annotation = entityIDs.length === 1 ?
t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) :
t('operations.rotate.annotation.multiple');
var _prevGraph;
var _prevAngle;
var _prevTransform;
var _pivot;
function doRotate() {
var fn;
if (context.graph() !== prevGraph) {
if (context.graph() !== _prevGraph) {
fn = context.perform;
} else {
fn = context.replace;
}
// projection changed, recalculate pivot
// projection changed, recalculate _pivot
var projection = context.projection;
var currTransform = projection.transform();
if (!prevTransform ||
currTransform.k !== prevTransform.k ||
currTransform.x !== prevTransform.x ||
currTransform.y !== prevTransform.y) {
if (!_prevTransform ||
currTransform.k !== _prevTransform.k ||
currTransform.x !== _prevTransform.x ||
currTransform.y !== _prevTransform.y) {
var nodes = utilGetAllNodes(entityIDs, context.graph()),
points = nodes.map(function(n) { return projection(n.loc); });
var nodes = utilGetAllNodes(entityIDs, context.graph());
var points = nodes.map(function(n) { return projection(n.loc); });
if (points.length === 1) { // degenerate case
pivot = points[0];
_pivot = points[0];
} else if (points.length === 2) {
pivot = geoInterp(points[0], points[1], 0.5);
_pivot = geoVecInterp(points[0], points[1], 0.5);
} else {
pivot = d3_polygonCentroid(d3_polygonHull(points));
_pivot = d3_polygonCentroid(d3_polygonHull(points));
}
prevAngle = undefined;
_prevAngle = undefined;
}
var currMouse = context.mouse(),
currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]);
var currMouse = context.mouse();
var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);
if (typeof prevAngle === 'undefined') prevAngle = currAngle;
var delta = currAngle - prevAngle;
if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;
var delta = currAngle - _prevAngle;
fn(actionRotate(entityIDs, pivot, delta, projection), annotation);
fn(actionRotate(entityIDs, _pivot, delta, projection), annotation);
prevTransform = currTransform;
prevAngle = currAngle;
prevGraph = context.graph();
_prevTransform = currTransform;
_prevAngle = currAngle;
_prevGraph = context.graph();
}
@@ -155,5 +156,12 @@ export function modeRotate(context, entityIDs) {
};
mode.selectedIDs = function() {
if (!arguments.length) return entityIDs;
// no assign
return mode;
};
return mode;
}
+10 -7
View File
@@ -63,7 +63,7 @@ export function modeSelect(context, selectedIDs) {
behaviorHover(context),
behaviorSelect(context),
behaviorLasso(context),
modeDragNode(context).selectedIDs(selectedIDs).behavior
modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior
],
inspector,
editMenu,
@@ -245,13 +245,16 @@ export function modeSelect(context, selectedIDs) {
function dblclick() {
var target = d3_select(d3_event.target),
datum = target.datum();
var target = d3_select(d3_event.target);
if (datum instanceof osmWay && !target.classed('fill')) {
var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection),
prev = datum.nodes[choice.index - 1],
next = datum.nodes[choice.index];
var datum = target.datum();
var entity = datum && datum.id && context.hasEntity(datum.id);
if (entity) datum = entity;
if (datum instanceof osmWay && target.classed('target')) {
var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection);
var prev = datum.nodes[choice.index - 1];
var next = datum.nodes[choice.index];
context.perform(
actionAddMidpoint({loc: choice.loc, edge: [prev, next]}, osmNode()),
+82 -1
View File
@@ -3,7 +3,7 @@ import _map from 'lodash-es/map';
import _some from 'lodash-es/some';
import { osmEntity } from './entity';
import { geoExtent } from '../geo';
import { geoAngle, geoExtent } from '../geo';
export function osmNode() {
@@ -49,6 +49,87 @@ _extend(osmNode.prototype, {
},
// Inspect tags and geometry to determine which direction(s) this node/vertex points
directions: function(resolver, projection) {
var val;
var i;
// which tag to use?
if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') {
// all-way stop tag on a highway intersection
val = 'all';
} else {
// generic direction tag
val = (this.tags.direction || '').toLowerCase();
// better suffix-style direction tag
var re = /:direction$/i;
var keys = Object.keys(this.tags);
for (i = 0; i < keys.length; i++) {
if (re.test(keys[i])) {
val = this.tags[keys[i]].toLowerCase();
break;
}
}
}
// swap cardinal for numeric directions
var cardinal = {
north: 0, n: 0,
northnortheast: 22, nne: 22,
northeast: 45, ne: 45,
eastnortheast: 67, ene: 67,
east: 90, e: 90,
eastsoutheast: 112, ese: 112,
southeast: 135, se: 135,
southsoutheast: 157, sse: 157,
south: 180, s: 180,
southsouthwest: 202, ssw: 202,
southwest: 225, sw: 225,
westsouthwest: 247, wsw: 247,
west: 270, w: 270,
westnorthwest: 292, wnw: 292,
northwest: 315, nw: 315,
northnorthwest: 337, nnw: 337
};
if (cardinal[val] !== undefined) {
val = cardinal[val];
}
// if direction is numeric, return early
if (val !== '' && !isNaN(+val)) {
return [(+val)];
}
var lookBackward =
(this.tags['traffic_sign:backward'] || val === 'backward' || val === 'both' || val === 'all');
var lookForward =
(this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all');
if (!lookForward && !lookBackward) return [];
var nodeIds = {};
resolver.parentWays(this).forEach(function(parent) {
var nodes = parent.nodes;
for (i = 0; i < nodes.length; i++) {
if (nodes[i] === this.id) { // match current entity
if (lookForward && i > 0) {
nodeIds[nodes[i - 1]] = true; // look back to prev node
}
if (lookBackward && i < nodes.length - 1) {
nodeIds[nodes[i + 1]] = true; // look ahead to next node
}
}
}
}, this);
return Object.keys(nodeIds).map(function(nodeId) {
// +90 because geoAngle returns angle from X axis, not Y (north)
return (geoAngle(this, resolver.entity(nodeId), projection) * (180 / Math.PI)) + 90;
}, this);
},
isEndpoint: function(resolver) {
return resolver.transient(this, 'isEndpoint', function() {
var id = this.id;
+9 -8
View File
@@ -4,7 +4,7 @@ import _uniq from 'lodash-es/uniq';
import { geoArea as d3_geoArea } from 'd3-geo';
import { geoExtent, geoCross } from '../geo';
import { geoExtent, geoVecCross } from '../geo';
import { osmEntity } from './entity';
import { osmLanes } from './lanes';
import { osmOneWayTags } from './tags';
@@ -133,15 +133,16 @@ _extend(osmWay.prototype, {
isConvex: function(resolver) {
if (!this.isClosed() || this.isDegenerate()) return null;
var nodes = _uniq(resolver.childNodes(this)),
coords = _map(nodes, 'loc'),
curr = 0, prev = 0;
var nodes = _uniq(resolver.childNodes(this));
var coords = _map(nodes, 'loc');
var curr = 0;
var prev = 0;
for (var i = 0; i < coords.length; i++) {
var o = coords[(i+1) % coords.length],
a = coords[i],
b = coords[(i+2) % coords.length],
res = geoCross(o, a, b);
var o = coords[(i+1) % coords.length];
var a = coords[i];
var b = coords[(i+2) % coords.length];
var res = geoVecCross(a, b, o);
curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
if (curr === 0) {
+51 -18
View File
@@ -183,33 +183,55 @@ export function rendererMap(context) {
if (map.editable() && !transformed) {
var hover = d3_event.target.__data__;
surface.selectAll('.data-layer-osm')
.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.call('drawn', this, {full: false});
.call(drawVertices.drawHover, context.graph(), hover, map.extent());
dispatch.call('drawn', this, { full: false });
}
})
.on('mouseout.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;
surface.selectAll('.data-layer-osm')
.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.call('drawn', this, {full: false});
.call(drawVertices.drawHover, context.graph(), hover, map.extent());
dispatch.call('drawn', this, { full: false });
}
});
supersurface
.call(context.background());
context.on('enter.map', function() {
context.on('enter.map', function() {
if (map.editable() && !transformed) {
var all = context.intersects(map.extent()),
filter = utilFunctor(true),
graph = context.graph();
all = context.features().filter(all, graph);
// redraw immediately any objects affected by a change in selectedIDs.
var graph = context.graph();
var selectedAndParents = {};
context.selectedIDs().forEach(function(id) {
var entity = graph.hasEntity(id);
if (entity) {
selectedAndParents[entity.id] = entity;
if (entity.type === 'node') {
graph.parentWays(entity).forEach(function(parent) {
selectedAndParents[parent.id] = parent;
});
}
}
});
var data = _values(selectedAndParents);
var filter = function(d) { return d.id in selectedAndParents; };
data = context.features().filter(data, graph);
surface.selectAll('.data-layer-osm')
.call(drawVertices, graph, all, filter, map.extent(), map.zoom())
.call(drawMidpoints, graph, all, filter, map.trimmedExtent());
dispatch.call('drawn', this, {full: false});
.call(drawVertices.drawSelected, graph, map.extent())
.call(drawLines, graph, data, filter)
.call(drawAreas, graph, data, filter)
.call(drawMidpoints, graph, data, filter, map.trimmedExtent());
dispatch.call('drawn', this, { full: false });
// redraw everything else later
scheduleRedraw();
}
});
@@ -265,10 +287,13 @@ export function rendererMap(context) {
function drawVector(difference, extent) {
var graph = context.graph(),
features = context.features(),
all = context.intersects(map.extent()),
data, filter;
var mode = context.mode();
var graph = context.graph();
var features = context.features();
var all = context.intersects(map.extent());
var fullRedraw = false;
var data;
var filter;
if (difference) {
var complete = difference.complete(map.extent());
@@ -290,18 +315,26 @@ export function rendererMap(context) {
} else {
data = all;
fullRedraw = true;
filter = utilFunctor(true);
}
}
data = features.filter(data, graph);
if (mode && mode.id === 'select') {
// update selected vertices - the user might have just double-clicked a way,
// creating a new vertex, triggering a partial redraw without a mode change
surface.selectAll('.data-layer-osm')
.call(drawVertices.drawSelected, graph, map.extent());
}
surface.selectAll('.data-layer-osm')
.call(drawVertices, graph, data, filter, map.extent(), map.zoom())
.call(drawVertices, graph, data, filter, map.extent(), fullRedraw)
.call(drawLines, graph, data, filter)
.call(drawAreas, graph, data, filter)
.call(drawMidpoints, graph, data, filter, map.trimmedExtent())
.call(drawLabels, graph, data, filter, dimensions, !difference && !extent)
.call(drawLabels, graph, data, filter, dimensions, fullRedraw)
.call(drawPoints, graph, data, filter);
dispatch.call('drawn', this, {full: true});
+59 -57
View File
@@ -2,28 +2,29 @@ import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoEuclideanDistance } from '../geo';
import { geoScaleToZoom, geoVecLength } from '../geo';
import { utilPrefixCSSProperty } from '../util';
export function rendererTileLayer(context) {
var tileSize = 256,
geotile = d3_geoTile(),
projection,
cache = {},
tileOrigin,
z,
transformProp = utilPrefixCSSProperty('Transform'),
source;
var tileSize = 256;
var transformProp = utilPrefixCSSProperty('Transform');
var geotile = d3_geoTile();
var _projection;
var _cache = {};
var _tileOrigin;
var _zoom;
var _source;
// blacklist overlay tiles around Null Island..
function nearNullIsland(x, y, z) {
if (z >= 7) {
var center = Math.pow(2, z - 1),
width = Math.pow(2, z - 6),
min = center - (width / 2),
max = center + (width / 2) - 1;
var center = Math.pow(2, z - 1);
var width = Math.pow(2, z - 6);
var min = center - (width / 2);
var max = center + (width / 2) - 1;
return x >= min && x <= max && y >= min && y <= max;
}
return false;
@@ -31,8 +32,8 @@ export function rendererTileLayer(context) {
function tileSizeAtZoom(d, z) {
var epsilon = 0.002;
return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon;
var EPSILON = 0.002;
return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON;
}
@@ -49,7 +50,7 @@ export function rendererTileLayer(context) {
function lookUp(d) {
for (var up = -1; up > -d[2]; up--) {
var tile = atZoom(d, up);
if (cache[source.url(tile)] !== false) {
if (_cache[_source.url(tile)] !== false) {
return tile;
}
}
@@ -57,7 +58,8 @@ export function rendererTileLayer(context) {
function uniqueBy(a, n) {
var o = [], seen = {};
var o = [];
var seen = {};
for (var i = 0; i < a.length; i++) {
if (seen[a[i][n]] === undefined) {
o.push(a[i]);
@@ -69,37 +71,37 @@ export function rendererTileLayer(context) {
function addSource(d) {
d.push(source.url(d));
d.push(_source.url(d));
return d;
}
// Update tiles based on current state of `projection`.
function background(selection) {
z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0);
_zoom = geoScaleToZoom(_projection.scale(), tileSize);
var pixelOffset;
if (source) {
if (_source) {
pixelOffset = [
source.offset()[0] * Math.pow(2, z),
source.offset()[1] * Math.pow(2, z)
_source.offset()[0] * Math.pow(2, _zoom),
_source.offset()[1] * Math.pow(2, _zoom)
];
} else {
pixelOffset = [0, 0];
}
var translate = [
projection.translate()[0] + pixelOffset[0],
projection.translate()[1] + pixelOffset[1]
_projection.translate()[0] + pixelOffset[0],
_projection.translate()[1] + pixelOffset[1]
];
geotile
.scale(projection.scale() * 2 * Math.PI)
.scale(_projection.scale() * 2 * Math.PI)
.translate(translate);
tileOrigin = [
projection.scale() * Math.PI - translate[0],
projection.scale() * Math.PI - translate[1]
_tileOrigin = [
_projection.scale() * Math.PI - translate[0],
_projection.scale() * Math.PI - translate[1]
];
render(selection);
@@ -107,36 +109,36 @@ export function rendererTileLayer(context) {
// Derive the tiles onscreen, remove those offscreen and position them.
// Important that this part not depend on `projection` because it's
// Important that this part not depend on `_projection` because it's
// rentered when tiles load/error (see #644).
function render(selection) {
if (!source) return;
if (!_source) return;
var requests = [];
var showDebug = context.getDebug('tile') && !source.overlay;
var showDebug = context.getDebug('tile') && !_source.overlay;
if (source.validZoom(z)) {
if (_source.validZoom(_zoom)) {
geotile().forEach(function(d) {
addSource(d);
if (d[3] === '') return;
if (typeof d[3] !== 'string') return; // Workaround for #2295
requests.push(d);
if (cache[d[3]] === false && lookUp(d)) {
if (_cache[d[3]] === false && lookUp(d)) {
requests.push(addSource(lookUp(d)));
}
});
requests = uniqueBy(requests, 3).filter(function(r) {
if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) {
if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) {
return false;
}
// don't re-request tiles which have failed in the past
return cache[r[3]] !== false;
return _cache[r[3]] !== false;
});
}
function load(d) {
cache[d[3]] = true;
_cache[d[3]] = true;
d3_select(this)
.on('error', null)
.on('load', null)
@@ -145,7 +147,7 @@ export function rendererTileLayer(context) {
}
function error(d) {
cache[d[3]] = false;
_cache[d[3]] = false;
d3_select(this)
.on('error', null)
.on('load', null)
@@ -154,19 +156,19 @@ export function rendererTileLayer(context) {
}
function imageTransform(d) {
var _ts = tileSize * Math.pow(2, z - d[2]);
var scale = tileSizeAtZoom(d, z);
var ts = tileSize * Math.pow(2, _zoom - d[2]);
var scale = tileSizeAtZoom(d, _zoom);
return 'translate(' +
((d[0] * _ts) - tileOrigin[0]) + 'px,' +
((d[1] * _ts) - tileOrigin[1]) + 'px) ' +
((d[0] * ts) - _tileOrigin[0]) + 'px,' +
((d[1] * ts) - _tileOrigin[1]) + 'px) ' +
'scale(' + scale + ',' + scale + ')';
}
function tileCenter(d) {
var _ts = tileSize * Math.pow(2, z - d[2]);
var ts = tileSize * Math.pow(2, _zoom - d[2]);
return [
((d[0] * _ts) - tileOrigin[0] + (_ts / 2)),
((d[1] * _ts) - tileOrigin[1] + (_ts / 2))
((d[0] * ts) - _tileOrigin[0] + (ts / 2)),
((d[1] * ts) - _tileOrigin[1] + (ts / 2))
];
}
@@ -178,14 +180,14 @@ export function rendererTileLayer(context) {
// Pick a representative tile near the center of the viewport
// (This is useful for sampling the imagery vintage)
var dims = geotile.size(),
mapCenter = [dims[0] / 2, dims[1] / 2],
minDist = Math.max(dims[0], dims[1]),
nearCenter;
var dims = geotile.size();
var mapCenter = [dims[0] / 2, dims[1] / 2];
var minDist = Math.max(dims[0], dims[1]);
var nearCenter;
requests.forEach(function(d) {
var c = tileCenter(d);
var dist = geoEuclideanDistance(c, mapCenter);
var dist = geoVecLength(c, mapCenter);
if (dist < minDist) {
minDist = dist;
nearCenter = d;
@@ -255,8 +257,8 @@ export function rendererTileLayer(context) {
.selectAll('.tile-label-debug-vintage')
.each(function(d) {
var span = d3_select(this);
var center = context.projection.invert(tileCenter(d));
source.getMetadata(center, d, function(err, result) {
var center = context._projection.invert(tileCenter(d));
_source.getMetadata(center, d, function(err, result) {
span.text((result && result.vintage && result.vintage.range) ||
t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown')
);
@@ -268,8 +270,8 @@ export function rendererTileLayer(context) {
background.projection = function(_) {
if (!arguments.length) return projection;
projection = _;
if (!arguments.length) return _projection;
_projection = _;
return background;
};
@@ -282,10 +284,10 @@ export function rendererTileLayer(context) {
background.source = function(_) {
if (!arguments.length) return source;
source = _;
cache = {};
geotile.scaleExtent(source.scaleExtent);
if (!arguments.length) return _source;
_source = _;
_cache = {};
geotile.scaleExtent(_source.scaleExtent);
return background;
};
+14 -14
View File
@@ -500,8 +500,8 @@ export default {
}
// update blacklists
var elements = xml.getElementsByTagName('blacklist'),
regexes = [];
var elements = xml.getElementsByTagName('blacklist');
var regexes = [];
for (var i = 0; i < elements.length; i++) {
var regex = elements[i].getAttribute('regex'); // needs unencode?
if (regex) {
@@ -516,8 +516,8 @@ export default {
if (rateLimitError) {
callback(rateLimitError, 'rateLimited');
} else {
var apiStatus = xml.getElementsByTagName('status'),
val = apiStatus[0].getAttribute('api');
var apiStatus = xml.getElementsByTagName('status');
var val = apiStatus[0].getAttribute('api');
callback(undefined, val);
}
@@ -544,14 +544,14 @@ export default {
loadTiles: function(projection, dimensions, callback) {
if (off) return;
var that = this,
s = projection.scale() * 2 * Math.PI,
z = Math.max(Math.log(s) / Math.log(2) - 8, 0),
ts = 256 * Math.pow(2, z - tileZoom),
origin = [
s / 2 - projection.translate()[0],
s / 2 - projection.translate()[1]
];
var that = this;
var s = projection.scale() * 2 * Math.PI;
var z = Math.max(Math.log(s) / Math.log(2) - 8, 0);
var ts = 256 * Math.pow(2, z - tileZoom);
var origin = [
s / 2 - projection.translate()[0],
s / 2 - projection.translate()[1]
];
var tiles = d3_geoTile()
.scaleExtent([tileZoom, tileZoom])
@@ -559,8 +559,8 @@ export default {
.size(dimensions)
.translate(projection.translate())()
.map(function(tile) {
var x = tile[0] * ts - origin[0],
y = tile[1] * ts - origin[1];
var x = tile[0] * ts - origin[0];
var y = tile[1] * ts - origin[1];
return {
id: tile.toString(),
+63 -4
View File
@@ -4,7 +4,7 @@ import _values from 'lodash-es/values';
import { bisector as d3_bisector } from 'd3-array';
import { osmEntity, osmIsSimpleMultipolygonOuterMember } from '../osm';
import { svgPath, svgTagClasses } from './index';
import { svgPath, svgSegmentWay, svgTagClasses } from './index';
export function svgAreas(projection, context) {
@@ -41,7 +41,59 @@ export function svgAreas(projection, context) {
}
return function drawAreas(selection, graph, entities, filter) {
function drawTargets(selection, graph, entities, filter) {
var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';
var getPath = svgPath(projection).geojson;
var activeID = context.activeID();
// The targets and nopes will be MultiLineString sub-segments of the ways
var data = { targets: [], nopes: [] };
entities.forEach(function(way) {
var features = svgSegmentWay(way, graph, activeID);
data.targets.push.apply(data.targets, features.passive);
data.nopes.push.apply(data.nopes, features.active);
});
// Targets allow hover and vertex snapping
var targets = selection.selectAll('.area.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('path')
.merge(targets)
.attr('d', getPath)
.attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; });
// NOPE
var nopes = selection.selectAll('.area.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('path')
.merge(nopes)
.attr('d', getPath)
.attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; });
}
function drawAreas(selection, graph, entities, filter) {
var path = svgPath(projection, graph, true),
areas = {},
multipolygon;
@@ -99,7 +151,7 @@ export function svgAreas(projection, context) {
.attr('d', path);
var layer = selection.selectAll('.layer-areas');
var layer = selection.selectAll('.layer-areas .layer-areas-areas');
var areagroup = layer
.selectAll('g.areagroup')
@@ -145,5 +197,12 @@ export function svgAreas(projection, context) {
})
.call(svgTagClasses())
.attr('d', path);
};
// touch targets
selection.selectAll('.layer-areas .layer-areas-targets')
.call(drawTargets, graph, data.stroke, filter);
}
return drawAreas;
}
+22 -23
View File
@@ -1,12 +1,8 @@
import { geoPath as d3_geoPath } from 'd3-geo';
import { select as d3_select } from 'd3-selection';
import { geoPolygonIntersectsPolygon } from '../geo';
import {
data,
dataImperial,
dataDriveLeft
} from '../../data';
import { data, dataImperial, dataDriveLeft } from '../../data';
import { svgPath } from './index';
export function svgDebug(projection, context) {
@@ -21,13 +17,12 @@ export function svgDebug(projection, context) {
}
function drawDebug(selection) {
var showsTile = context.getDebug('tile'),
showsCollision = context.getDebug('collision'),
showsImagery = context.getDebug('imagery'),
showsImperial = context.getDebug('imperial'),
showsDriveLeft = context.getDebug('driveLeft'),
path = d3_geoPath(projection);
var showsTile = context.getDebug('tile');
var showsCollision = context.getDebug('collision');
var showsImagery = context.getDebug('imagery');
var showsImperial = context.getDebug('imperial');
var showsDriveLeft = context.getDebug('driveLeft');
var showsTouchTargets = context.getDebug('target');
var debugData = [];
if (showsTile) {
@@ -45,6 +40,9 @@ export function svgDebug(projection, context) {
if (showsDriveLeft) {
debugData.push({ class: 'green', label: 'driveLeft' });
}
if (showsTouchTargets) {
debugData.push({ class: 'pink', label: 'touchTargets' });
}
var legend = d3_select('#content')
@@ -84,14 +82,14 @@ export function svgDebug(projection, context) {
.merge(layer);
var extent = context.map().extent(),
dataImagery = data.imagery || [],
availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) {
if (!source.polygon) return false;
return source.polygon.some(function(polygon) {
return geoPolygonIntersectsPolygon(polygon, extent, true);
});
}));
var extent = context.map().extent();
var dataImagery = data.imagery || [];
var availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) {
if (!source.polygon) return false;
return source.polygon.some(function(polygon) {
return geoPolygonIntersectsPolygon(polygon, extent, true);
});
}));
var imagery = layer.selectAll('path.debug-imagery')
.data(showsImagery ? availableImagery : []);
@@ -130,7 +128,7 @@ export function svgDebug(projection, context) {
// update
layer.selectAll('path')
.attr('d', path);
.attr('d', svgPath(projection).geojson);
}
@@ -142,7 +140,8 @@ export function svgDebug(projection, context) {
context.getDebug('collision') ||
context.getDebug('imagery') ||
context.getDebug('imperial') ||
context.getDebug('driveLeft');
context.getDebug('driveLeft') ||
context.getDebug('target');
} else {
return this;
}
+57 -31
View File
@@ -26,24 +26,61 @@ export function svgDefs(context) {
return function drawDefs(selection) {
var defs = selection.append('defs');
// marker
defs.append('marker')
// markers
defs
.append('marker')
.attr('id', 'oneway-marker')
.attr('viewBox', '0 0 10 10')
.attr('refY', 2.5)
.attr('viewBox', '0 0 10 5')
.attr('refX', 5)
.attr('refY', 2.5)
.attr('markerWidth', 2)
.attr('markerHeight', 2)
.attr('markerUnits', 'strokeWidth')
.attr('orient', 'auto')
.append('path')
.attr('class', 'oneway')
.attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z')
.attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z')
.attr('stroke', 'none')
.attr('fill', '#000')
.attr('opacity', '0.75');
defs
.append('marker')
.attr('id', 'viewfield-marker')
.attr('viewBox', '0 0 16 16')
.attr('refX', 8)
.attr('refY', 16)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('markerUnits', 'strokeWidth')
.attr('orient', 'auto')
.append('path')
.attr('class', 'viewfield')
.attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')
.attr('fill', '#333')
.attr('fill-opacity', '0.75')
.attr('stroke', '#fff')
.attr('stroke-width', '0.5px')
.attr('stroke-opacity', '0.75');
defs
.append('marker')
.attr('id', 'viewfield-marker-wireframe')
.attr('viewBox', '0 0 16 16')
.attr('refX', 8)
.attr('refY', 16)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('markerUnits', 'strokeWidth')
.attr('orient', 'auto')
.append('path')
.attr('class', 'viewfield')
.attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')
.attr('fill', 'none')
.attr('stroke', '#fff')
.attr('stroke-width', '0.5px')
.attr('stroke-opacity', '0.75');
// patterns
var patterns = defs.selectAll('pattern')
.data([
@@ -59,23 +96,21 @@ export function svgDefs(context) {
])
.enter()
.append('pattern')
.attr('id', function (d) {
return 'pattern-' + d[0];
})
.attr('id', function (d) { return 'pattern-' + d[0]; })
.attr('width', 32)
.attr('height', 32)
.attr('patternUnits', 'userSpaceOnUse');
patterns.append('rect')
patterns
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 32)
.attr('height', 32)
.attr('class', function (d) {
return 'pattern-color-' + d[0];
});
.attr('class', function (d) { return 'pattern-color-' + d[0]; });
patterns.append('image')
patterns
.append('image')
.attr('x', 0)
.attr('y', 0)
.attr('width', 32)
@@ -85,29 +120,20 @@ export function svgDefs(context) {
});
// clip paths
defs.selectAll()
defs.selectAll('clipPath')
.data([12, 18, 20, 32, 45])
.enter()
.append('clipPath')
.attr('id', function (d) {
return 'clip-square-' + d;
})
.attr('id', function (d) { return 'clip-square-' + d; })
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', function (d) {
return d;
})
.attr('height', function (d) {
return d;
});
.attr('width', function (d) { return d; })
.attr('height', function (d) { return d; });
defs.call(SVGSpriteDefinition(
'iD-sprite',
context.imagePath('iD-sprite.svg')));
defs.call(SVGSpriteDefinition(
'maki-sprite',
context.imagePath('maki-sprite.svg')));
// symbol spritesheets
defs
.call(SVGSpriteDefinition('iD-sprite', context.imagePath('iD-sprite.svg')))
.call(SVGSpriteDefinition('maki-sprite', context.imagePath('maki-sprite.svg')));
};
}
+286
View File
@@ -0,0 +1,286 @@
import _extend from 'lodash-es/extend';
import {
geoIdentity as d3_geoIdentity,
geoPath as d3_geoPath,
geoStream as d3_geoStream
} from 'd3-geo';
import { geoVecLength } from '../geo';
// Touch targets control which other vertices we can drag a vertex onto.
//
// - the activeID - nope
// - 1 away (adjacent) to the activeID - yes (vertices will be merged)
// - 2 away from the activeID - nope (would create a self intersecting segment)
// - all others on a linear way - yes
// - all others on a closed way - nope (would create a self intersecting polygon)
//
// returns
// 0 = active vertex - no touch/connect
// 1 = passive vertex - yes touch/connect
// 2 = adjacent vertex - yes but pay attention segmenting a line here
//
export function svgPassiveVertex(node, graph, activeID) {
if (!activeID) return 1;
if (activeID === node.id) return 0;
var parents = graph.parentWays(node);
for (var i = 0; i < parents.length; i++) {
var nodes = parents[i].nodes;
var isClosed = parents[i].isClosed();
for (var j = 0; j < nodes.length; j++) { // find this vertex, look nearby
if (nodes[j] === node.id) {
var ix1 = j - 2;
var ix2 = j - 1;
var ix3 = j + 1;
var ix4 = j + 2;
if (isClosed) { // wraparound if needed
var max = nodes.length - 1;
if (ix1 < 0) ix1 = max + ix1;
if (ix2 < 0) ix2 = max + ix2;
if (ix3 > max) ix3 = ix3 - max;
if (ix4 > max) ix4 = ix4 - max;
}
if (nodes[ix1] === activeID) return 0; // no - prevent self intersect
else if (nodes[ix2] === activeID) return 2; // ok - adjacent
else if (nodes[ix3] === activeID) return 2; // ok - adjacent
else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect
else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect
}
}
}
return 1; // ok
}
export function svgOneWaySegments(projection, graph, dt) {
return function(entity) {
var i = 0;
var offset = dt;
var segments = [];
var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream;
var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });
var a, b;
if (entity.tags.oneway === '-1') {
coordinates.reverse();
}
d3_geoStream({
type: 'LineString',
coordinates: coordinates
}, projection.stream(clip({
lineStart: function() {},
lineEnd: function() { a = null; },
point: function(x, y) {
b = [x, y];
if (a) {
var span = geoVecLength(a, b) - offset;
if (span >= 0) {
var angle = Math.atan2(b[1] - a[1], b[0] - a[0]);
var dx = dt * Math.cos(angle);
var dy = dt * Math.sin(angle);
var p = [
a[0] + offset * Math.cos(angle),
a[1] + offset * Math.sin(angle)
];
var segment = 'M' + a[0] + ',' + a[1] + 'L' + p[0] + ',' + p[1];
for (span -= dt; span >= 0; span -= dt) {
p[0] += dx;
p[1] += dy;
segment += 'L' + p[0] + ',' + p[1];
}
segment += 'L' + b[0] + ',' + b[1];
segments.push({id: entity.id, index: i, d: segment});
}
offset = -span;
i++;
}
a = b;
}
})));
return segments;
};
}
export function svgPath(projection, graph, isArea) {
// Explanation of magic numbers:
// "padding" here allows space for strokes to extend beyond the viewport,
// so that the stroke isn't drawn along the edge of the viewport when
// the shape is clipped.
//
// When drawing lines, pad viewport by 5px.
// When drawing areas, pad viewport by 65px in each direction to allow
// for 60px area fill stroke (see ".fill-partial path.fill" css rule)
var cache = {};
var padding = isArea ? 65 : 5;
var viewport = projection.clipExtent();
var paddedExtent = [
[viewport[0][0] - padding, viewport[0][1] - padding],
[viewport[1][0] + padding, viewport[1][1] + padding]
];
var clip = d3_geoIdentity().clipExtent(paddedExtent).stream;
var project = projection.stream;
var path = d3_geoPath()
.projection({stream: function(output) { return project(clip(output)); }});
var svgpath = function(entity) {
if (entity.id in cache) {
return cache[entity.id];
} else {
return cache[entity.id] = path(entity.asGeoJSON(graph));
}
};
svgpath.geojson = path;
return svgpath;
}
export function svgPointTransform(projection) {
var svgpoint = function(entity) {
// http://jsperf.com/short-array-join
var pt = projection(entity.loc);
return 'translate(' + pt[0] + ',' + pt[1] + ')';
};
svgpoint.geojson = function(d) {
return svgpoint(d.properties.entity);
};
return svgpoint;
}
export function svgRelationMemberTags(graph) {
return function(entity) {
var tags = entity.tags;
graph.parentRelations(entity).forEach(function(relation) {
var type = relation.tags.type;
if (type === 'multipolygon' || type === 'boundary') {
tags = _extend({}, relation.tags, tags);
}
});
return tags;
};
}
export function svgSegmentWay(way, graph, activeID) {
var features = { passive: [], active: [] };
var coordGroups = { passive: [], active: [] };
var nodeGroups = { passive: [], active: [] };
var coords = [];
var nodes = [];
var startType = null; // 0 = active, 1 = passive, 2 = adjacent
var currType = null; // 0 = active, 1 = passive, 2 = adjacent
var node;
for (var i = 0; i < way.nodes.length; i++) {
if (way.nodes[i] === activeID) { // vertex is the activeID
coords = []; // draw no segment here
nodes = [];
startType = null;
continue;
}
node = graph.entity(way.nodes[i]);
currType = svgPassiveVertex(node, graph, activeID);
if (startType === null) {
startType = currType;
}
if (currType !== startType) { // line changes here - try to save a segment
if (coords.length > 0) { // finish previous segment
coords.push(node.loc);
nodes.push(node.id);
if (startType === 2 || currType === 2) { // one adjacent vertex
coordGroups.active.push(coords);
nodeGroups.active.push(nodes);
} else if (startType === 0 && currType === 0) { // both active vertices
coordGroups.active.push(coords);
nodeGroups.active.push(nodes);
} else {
coordGroups.passive.push(coords);
nodeGroups.passive.push(nodes);
}
}
coords = [];
nodes = [];
startType = currType;
}
coords.push(node.loc);
nodes.push(node.id);
}
// complete whatever segment we ended on
if (coords.length > 1) {
if (startType === 2 || currType === 2) { // one adjacent vertex
coordGroups.active.push(coords);
nodeGroups.active.push(nodes);
} else if (startType === 0 && currType === 0) { // both active vertices
coordGroups.active.push(coords);
nodeGroups.active.push(nodes);
} else {
coordGroups.passive.push(coords);
nodeGroups.passive.push(nodes);
}
}
if (coordGroups.passive.length) {
features.passive.push({
type: 'Feature',
id: way.id,
properties: {
target: true,
entity: way,
nodes: nodeGroups.passive
},
geometry: {
type: 'MultiLineString',
coordinates: coordGroups.passive
}
});
}
if (coordGroups.active.length) {
features.active.push({
type: 'Feature',
id: way.id + '-nope', // break the ids on purpose
properties: {
target: true,
entity: way,
nodes: nodeGroups.active,
nope: true,
originalID: way.id
},
geometry: {
type: 'MultiLineString',
coordinates: coordGroups.active
}
});
}
return features;
}
+6 -4
View File
@@ -9,13 +9,15 @@ export { svgLines } from './lines.js';
export { svgMapillaryImages } from './mapillary_images.js';
export { svgMapillarySigns } from './mapillary_signs.js';
export { svgMidpoints } from './midpoints.js';
export { svgOneWaySegments } from './one_way_segments.js';
export { svgOneWaySegments } from './helpers.js';
export { svgOpenstreetcamImages } from './openstreetcam_images.js';
export { svgOsm } from './osm.js';
export { svgPath } from './path.js';
export { svgPointTransform } from './point_transform.js';
export { svgPassiveVertex } from './helpers.js';
export { svgPath } from './helpers.js';
export { svgPointTransform } from './helpers.js';
export { svgPoints } from './points.js';
export { svgRelationMemberTags } from './relation_member_tags.js';
export { svgRelationMemberTags } from './helpers.js';
export { svgSegmentWay } from './helpers.js';
export { svgTagClasses } from './tag_classes.js';
export { svgTurns } from './turns.js';
export { svgVertices } from './vertices.js';
+272 -182
View File
@@ -10,10 +10,11 @@ import { textDirection } from '../util/locale';
import {
geoExtent,
geoEuclideanDistance,
geoInterp,
geoPolygonIntersectsPolygon,
geoPathLength
geoPathLength,
geoScaleToZoom,
geoVecInterp,
geoVecLength
} from '../geo';
import { osmEntity } from '../osm';
@@ -27,14 +28,15 @@ import {
} from '../util';
export function svgLabels(projection, context) {
var path = d3_geoPath(projection),
detected = utilDetect(),
baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'),
rdrawn = rbush(),
rskipped = rbush(),
textWidthCache = {},
entitybboxes = {};
var path = d3_geoPath(projection);
var detected = utilDetect();
var baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge');
var _rdrawn = rbush();
var _rskipped = rbush();
var _textWidthCache = {};
var _entitybboxes = {};
// Listed from highest to lowest priority
var labelStack = [
@@ -87,8 +89,8 @@ export function svgLabels(projection, context) {
function textWidth(text, size, elem) {
var c = textWidthCache[size];
if (!c) c = textWidthCache[size] = {};
var c = _textWidthCache[size];
if (!c) c = _textWidthCache[size] = {};
if (c[text]) {
return c[text];
@@ -113,9 +115,11 @@ export function svgLabels(projection, context) {
.filter(filter)
.data(entities, osmEntity.key);
// exit
paths.exit()
.remove();
// enter/update
paths.enter()
.append('path')
.style('stroke-width', get(labels, 'font-size'))
@@ -131,9 +135,11 @@ export function svgLabels(projection, context) {
.filter(filter)
.data(entities, osmEntity.key);
// exit
texts.exit()
.remove();
// enter
texts.enter()
.append('text')
.attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; })
@@ -141,9 +147,8 @@ export function svgLabels(projection, context) {
.append('textPath')
.attr('class', 'textpath');
texts = selection.selectAll('text.' + classes);
texts.selectAll('.textpath')
// update
selection.selectAll('text.' + classes).selectAll('.textpath')
.filter(filter)
.data(entities, osmEntity.key)
.attr('startOffset', '50%')
@@ -157,17 +162,17 @@ export function svgLabels(projection, context) {
.filter(filter)
.data(entities, osmEntity.key);
// exit
texts.exit()
.remove();
texts = texts.enter()
// enter/update
texts.enter()
.append('text')
.attr('class', function(d, i) {
return classes + ' ' + labels[i].classes + ' ' + d.id;
})
.merge(texts);
texts
.merge(texts)
.attr('x', get(labels, 'x'))
.attr('y', get(labels, 'y'))
.style('text-anchor', get(labels, 'textAnchor'))
@@ -194,25 +199,25 @@ export function svgLabels(projection, context) {
.filter(filter)
.data(entities, osmEntity.key);
// exit
icons.exit()
.remove();
icons = icons.enter()
// enter/update
icons.enter()
.append('use')
.attr('class', 'icon ' + classes)
.attr('width', '17px')
.attr('height', '17px')
.merge(icons);
icons
.merge(icons)
.attr('transform', get(labels, 'transform'))
.attr('xlink:href', function(d) {
var preset = context.presets().match(d, context.graph()),
picon = preset && preset.icon;
var preset = context.presets().match(d, context.graph());
var picon = preset && preset.icon;
if (!picon)
if (!picon) {
return '';
else {
} else {
var isMaki = dataFeatureIcons.indexOf(picon) !== -1;
return '#' + picon + (isMaki ? '-15' : '');
}
@@ -221,23 +226,11 @@ export function svgLabels(projection, context) {
function drawCollisionBoxes(selection, rtree, which) {
var showDebug = context.getDebug('collision'),
classes = 'debug ' + which + ' ' +
(which === 'debug-skipped' ? 'orange' : 'yellow');
var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');
var debug = selection.selectAll('.layer-label-debug')
.data(showDebug ? [true] : []);
debug.exit()
.remove();
debug = debug.enter()
.append('g')
.attr('class', 'layer-label-debug')
.merge(debug);
if (showDebug) {
var gj = rtree.all().map(function(d) {
var gj = [];
if (context.getDebug('collision')) {
gj = rtree.all().map(function(d) {
return { type: 'Polygon', coordinates: [[
[d.minX, d.minY],
[d.maxX, d.minY],
@@ -246,67 +239,102 @@ export function svgLabels(projection, context) {
[d.minX, d.minY]
]]};
});
var debugboxes = debug.selectAll('.' + which)
.data(gj);
debugboxes.exit()
.remove();
debugboxes = debugboxes.enter()
.append('path')
.attr('class', classes)
.merge(debugboxes);
debugboxes
.attr('d', d3_geoPath());
}
var boxes = selection.selectAll('.' + which)
.data(gj);
// exit
boxes.exit()
.remove();
// enter/update
boxes.enter()
.append('path')
.attr('class', classes)
.merge(boxes)
.attr('d', d3_geoPath());
}
function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {
var lowZoom = context.surface().classed('low-zoom');
var wireframe = context.surface().classed('fill-wireframe');
var zoom = geoScaleToZoom(projection.scale());
var labelable = [];
var renderNodeAs = {};
var i, j, k, entity, geometry;
var labelable = [], i, j, k, entity, geometry;
for (i = 0; i < labelStack.length; i++) {
labelable.push([]);
}
if (fullRedraw) {
rdrawn.clear();
rskipped.clear();
entitybboxes = {};
_rdrawn.clear();
_rskipped.clear();
_entitybboxes = {};
} else {
for (i = 0; i < entities.length; i++) {
entity = entities[i];
var toRemove = []
.concat(entitybboxes[entity.id] || [])
.concat(entitybboxes[entity.id + 'I'] || []);
.concat(_entitybboxes[entity.id] || [])
.concat(_entitybboxes[entity.id + 'I'] || []);
for (j = 0; j < toRemove.length; j++) {
rdrawn.remove(toRemove[j]);
rskipped.remove(toRemove[j]);
_rdrawn.remove(toRemove[j]);
_rskipped.remove(toRemove[j]);
}
}
}
// Split entities into groups specified by labelStack
// Loop through all the entities to do some preprocessing
for (i = 0; i < entities.length; i++) {
entity = entities[i];
geometry = entity.geometry(graph);
if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point
var preset = geometry === 'area' && context.presets().match(entity, graph),
icon = preset && !blacklisted(preset) && preset.icon;
// Insert collision boxes around interesting points/vertices
if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) {
var hasDirections = entity.directions(graph, projection).length;
var markerPadding;
if (!wireframe && geometry === 'point' && !(zoom >= 18 && hasDirections)) {
renderNodeAs[entity.id] = 'point';
markerPadding = 20; // extra y for marker height
} else {
renderNodeAs[entity.id] = 'vertex';
markerPadding = 0;
}
var coord = projection(entity.loc);
var nodePadding = 10;
var bbox = {
minX: coord[0] - nodePadding,
minY: coord[1] - nodePadding - markerPadding,
maxX: coord[0] + nodePadding,
maxY: coord[1] + nodePadding
};
doInsert(bbox, entity.id + 'P');
}
// From here on, treat vertices like points
if (geometry === 'vertex') {
geometry = 'point';
}
// Determine which entities are label-able
var preset = geometry === 'area' && context.presets().match(entity, graph);
var icon = preset && !blacklisted(preset) && preset.icon;
if (!icon && !utilDisplayName(entity))
continue;
for (k = 0; k < labelStack.length; k++) {
var matchGeom = labelStack[k][0],
matchKey = labelStack[k][1],
matchVal = labelStack[k][2],
hasVal = entity.tags[matchKey];
var matchGeom = labelStack[k][0];
var matchKey = labelStack[k][1];
var matchVal = labelStack[k][2];
var hasVal = entity.tags[matchKey];
if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) {
labelable[k].push(entity);
@@ -330,22 +358,28 @@ export function svgLabels(projection, context) {
// Try and find a valid label for labellable entities
for (k = 0; k < labelable.length; k++) {
var fontSize = labelStack[k][3];
for (i = 0; i < labelable[k].length; i++) {
entity = labelable[k][i];
geometry = entity.geometry(graph);
var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName,
name = getName(entity),
width = name && textWidth(name, fontSize),
p = null;
var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName;
var name = getName(entity);
var width = name && textWidth(name, fontSize);
var p = null;
if (geometry === 'point' || geometry === 'vertex') {
// no point or vertex labels in wireframe mode
// no vertex labels at low zooms (vertices have no icons)
if (wireframe) continue;
var renderAs = renderNodeAs[entity.id];
if (renderAs === 'vertex' && zoom < 17) continue;
p = getPointLabel(entity, width, fontSize, renderAs);
if (geometry === 'point') {
p = getPointLabel(entity, width, fontSize, geometry);
} else if (geometry === 'vertex' && !lowZoom) {
// don't label vertices at low zoom because they don't have icons
p = getPointLabel(entity, width, fontSize, geometry);
} else if (geometry === 'line') {
p = getLineLabel(entity, width, fontSize);
} else if (geometry === 'area') {
p = getAreaLabel(entity, width, fontSize);
}
@@ -360,38 +394,52 @@ export function svgLabels(projection, context) {
}
function isInterestingVertex(entity) {
var selectedIDs = context.selectedIDs();
return entity.hasInterestingTags() ||
entity.isEndpoint(graph) ||
entity.isConnected(graph) ||
selectedIDs.indexOf(entity.id) !== -1 ||
_some(graph.parentWays(entity), function(parent) {
return selectedIDs.indexOf(parent.id) !== -1;
});
}
function getPointLabel(entity, width, height, geometry) {
var y = (geometry === 'point' ? -12 : 0),
pointOffsets = {
ltr: [15, y, 'start'],
rtl: [-15, y, 'end']
};
var y = (geometry === 'point' ? -12 : 0);
var pointOffsets = {
ltr: [15, y, 'start'],
rtl: [-15, y, 'end']
};
var coord = projection(entity.loc),
margin = 2,
offset = pointOffsets[textDirection],
p = {
height: height,
width: width,
x: coord[0] + offset[0],
y: coord[1] + offset[1],
textAnchor: offset[2]
},
bbox;
var coord = projection(entity.loc);
var textPadding = 2;
var offset = pointOffsets[textDirection];
var p = {
height: height,
width: width,
x: coord[0] + offset[0],
y: coord[1] + offset[1],
textAnchor: offset[2]
};
// insert a collision box for the text label..
var bbox;
if (textDirection === 'rtl') {
bbox = {
minX: p.x - width - margin,
minY: p.y - (height / 2) - margin,
maxX: p.x + margin,
maxY: p.y + (height / 2) + margin
minX: p.x - width - textPadding,
minY: p.y - (height / 2) - textPadding,
maxX: p.x + textPadding,
maxY: p.y + (height / 2) + textPadding
};
} else {
bbox = {
minX: p.x - margin,
minY: p.y - (height / 2) - margin,
maxX: p.x + width + margin,
maxY: p.y + (height / 2) + margin
minX: p.x - textPadding,
minY: p.y - (height / 2) - textPadding,
maxX: p.x + width + textPadding,
maxY: p.y + (height / 2) + textPadding
};
}
@@ -402,26 +450,28 @@ export function svgLabels(projection, context) {
function getLineLabel(entity, width, height) {
var viewport = geoExtent(context.projection.clipExtent()).polygon(),
nodes = _map(graph.childNodes(entity), 'loc').map(projection),
length = geoPathLength(nodes);
var viewport = geoExtent(context.projection.clipExtent()).polygon();
var points = _map(graph.childNodes(entity), 'loc').map(projection);
var length = geoPathLength(points);
if (length < width + 20) return;
// todo: properly clip points to viewport
// % along the line to attempt to place the label
var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,
25, 75, 20, 80, 15, 95, 10, 90, 5, 95];
var margin = 3;
var padding = 3;
for (var i = 0; i < lineOffsets.length; i++) {
var offset = lineOffsets[i],
middle = offset / 100 * length,
start = middle - width / 2;
var offset = lineOffsets[i];
var middle = offset / 100 * length;
var start = middle - width / 2;
if (start < 0 || start + width > length) continue;
// generate subpath and ignore paths that are invalid or don't cross viewport.
var sub = subpath(nodes, start, start + width);
var sub = subpath(points, start, start + width);
if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {
continue;
}
@@ -431,20 +481,22 @@ export function svgLabels(projection, context) {
sub = sub.reverse();
}
var bboxes = [],
boxsize = (height + 2) / 2;
var bboxes = [];
var boxsize = (height + 2) / 2;
for (var j = 0; j < sub.length - 1; j++) {
var a = sub[j];
var b = sub[j + 1];
var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2));
// split up the text into small collision boxes
var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));
for (var box = 0; box < num; box++) {
var p = geoInterp(a, b, box / num);
var x0 = p[0] - boxsize - margin;
var y0 = p[1] - boxsize - margin;
var x1 = p[0] + boxsize + margin;
var y1 = p[1] + boxsize + margin;
var p = geoVecInterp(a, b, box / num);
var x0 = p[0] - boxsize - padding;
var y0 = p[1] - boxsize - padding;
var x1 = p[0] + boxsize + padding;
var y1 = p[1] + boxsize + padding;
bboxes.push({
minX: Math.min(x0, x1),
@@ -455,7 +507,7 @@ export function svgLabels(projection, context) {
}
}
if (tryInsert(bboxes, entity.id, false)) {
if (tryInsert(bboxes, entity.id, false)) { // accept this one
return {
'font-size': height + 2,
lineString: lineString(sub),
@@ -469,18 +521,18 @@ export function svgLabels(projection, context) {
return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);
}
function lineString(nodes) {
return 'M' + nodes.join('L');
function lineString(points) {
return 'M' + points.join('L');
}
function subpath(nodes, from, to) {
var sofar = 0,
start, end, i0, i1;
function subpath(points, from, to) {
var sofar = 0;
var start, end, i0, i1;
for (var i = 0; i < nodes.length - 1; i++) {
var a = nodes[i],
b = nodes[i + 1];
var current = geoEuclideanDistance(a, b);
for (var i = 0; i < points.length - 1; i++) {
var a = points[i];
var b = points[i + 1];
var current = geoVecLength(a, b);
var portion;
if (!start && sofar + current >= from) {
portion = (from - sofar) / current;
@@ -501,30 +553,30 @@ export function svgLabels(projection, context) {
sofar += current;
}
var ret = nodes.slice(i0, i1);
ret.unshift(start);
ret.push(end);
return ret;
var result = points.slice(i0, i1);
result.unshift(start);
result.push(end);
return result;
}
}
function getAreaLabel(entity, width, height) {
var centroid = path.centroid(entity.asGeoJSON(graph, true)),
extent = entity.extent(graph),
areaWidth = projection(extent[1])[0] - projection(extent[0])[0];
var centroid = path.centroid(entity.asGeoJSON(graph, true));
var extent = entity.extent(graph);
var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];
if (isNaN(centroid[0]) || areaWidth < 20) return;
var preset = context.presets().match(entity, context.graph()),
picon = preset && preset.icon,
iconSize = 17,
margin = 2,
p = {};
var preset = context.presets().match(entity, context.graph());
var picon = preset && preset.icon;
var iconSize = 17;
var padding = 2;
var p = {};
if (picon) { // icon and label..
if (addIcon()) {
addLabel(iconSize + margin);
addLabel(iconSize + padding);
return p;
}
} else { // label only..
@@ -556,10 +608,10 @@ export function svgLabels(projection, context) {
var labelX = centroid[0];
var labelY = centroid[1] + yOffset;
var bbox = {
minX: labelX - (width / 2) - margin,
minY: labelY - (height / 2) - margin,
maxX: labelX + (width / 2) + margin,
maxY: labelY + (height / 2) + margin
minX: labelX - (width / 2) - padding,
minY: labelY - (height / 2) - padding,
maxX: labelX + (width / 2) + padding,
maxY: labelY + (height / 2) + padding
};
if (tryInsert([bbox], entity.id, true)) {
@@ -575,12 +627,25 @@ export function svgLabels(projection, context) {
}
// force insert a singular bounding box
// singular box only, no array, id better be unique
function doInsert(bbox, id) {
bbox.id = id;
var oldbox = _entitybboxes[id];
if (oldbox) {
_rdrawn.remove(oldbox);
}
_entitybboxes[id] = bbox;
_rdrawn.insert(bbox);
}
function tryInsert(bboxes, id, saveSkipped) {
var skipped = false,
bbox;
var skipped = false;
for (var i = 0; i < bboxes.length; i++) {
bbox = bboxes[i];
var bbox = bboxes[i];
bbox.id = id;
// Check that label is visible
@@ -588,28 +653,30 @@ export function svgLabels(projection, context) {
skipped = true;
break;
}
if (rdrawn.collides(bbox)) {
if (_rdrawn.collides(bbox)) {
skipped = true;
break;
}
}
entitybboxes[id] = bboxes;
_entitybboxes[id] = bboxes;
if (skipped) {
if (saveSkipped) {
rskipped.load(bboxes);
_rskipped.load(bboxes);
}
} else {
rdrawn.load(bboxes);
_rdrawn.load(bboxes);
}
return !skipped;
}
var label = selection.selectAll('.layer-label'),
halo = selection.selectAll('.layer-halo');
var layer = selection.selectAll('.layer-labels');
var halo = layer.selectAll('.layer-labels-halo');
var label = layer.selectAll('.layer-labels-label');
var debug = layer.selectAll('.layer-labels-debug');
// points
drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point);
@@ -627,51 +694,74 @@ export function svgLabels(projection, context) {
drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area);
// debug
drawCollisionBoxes(label, rskipped, 'debug-skipped');
drawCollisionBoxes(label, rdrawn, 'debug-drawn');
drawCollisionBoxes(debug, _rskipped, 'debug-skipped');
drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');
selection.call(filterLabels);
layer.call(filterLabels);
}
function filterLabels(selection) {
var layers = selection
.selectAll('.layer-label, .layer-halo');
.selectAll('.layer-labels-label, .layer-labels-halo');
layers.selectAll('.proximate')
.classed('proximate', false);
layers.selectAll('.nolabel')
.classed('nolabel', false);
var mouse = context.mouse(),
graph = context.graph(),
selectedIDs = context.selectedIDs(),
ids = [],
pad, bbox;
var mouse = context.mouse();
var graph = context.graph();
var selectedIDs = context.selectedIDs();
var ids = [];
var pad, bbox;
// hide labels near the mouse
if (mouse) {
pad = 20;
bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };
ids.push.apply(ids, _map(rdrawn.search(bbox), 'id'));
ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id'));
}
// hide labels along selected ways, or near selected vertices
// hide labels on selected nodes (they look weird when dragging / haloed)
for (var i = 0; i < selectedIDs.length; i++) {
var entity = graph.hasEntity(selectedIDs[i]);
if (!entity) continue;
var geometry = entity.geometry(graph);
if (geometry === 'line') {
if (entity && entity.type === 'node') {
ids.push(selectedIDs[i]);
} else if (geometry === 'vertex') {
var point = context.projection(entity.loc);
pad = 10;
bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad };
ids.push.apply(ids, _map(rdrawn.search(bbox), 'id'));
}
}
layers.selectAll(utilEntitySelector(ids))
.classed('proximate', true);
.classed('nolabel', true);
// draw the mouse bbox if debugging is on..
var debug = selection.selectAll('.layer-labels-debug');
var gj = [];
if (context.getDebug('collision')) {
gj = bbox ? [{
type: 'Polygon',
coordinates: [[
[bbox.minX, bbox.minY],
[bbox.maxX, bbox.minY],
[bbox.maxX, bbox.maxY],
[bbox.minX, bbox.maxY],
[bbox.minX, bbox.minY]
]]
}] : [];
}
var box = debug.selectAll('.debug-mouse')
.data(gj);
// exit
box.exit()
.remove();
// enter/update
box.enter()
.append('path')
.attr('class', 'debug debug-mouse yellow')
.merge(box)
.attr('d', d3_geoPath());
}
+78 -17
View File
@@ -10,6 +10,7 @@ import {
svgOneWaySegments,
svgPath,
svgRelationMemberTags,
svgSegmentWay,
svgTagClasses
} from './index';
@@ -36,13 +37,63 @@ export function svgLines(projection, context) {
};
function drawTargets(selection, graph, entities, filter) {
var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';
var getPath = svgPath(projection).geojson;
var activeID = context.activeID();
// The targets and nopes will be MultiLineString sub-segments of the ways
var data = { targets: [], nopes: [] };
entities.forEach(function(way) {
var features = svgSegmentWay(way, graph, activeID);
data.targets.push.apply(data.targets, features.passive);
data.nopes.push.apply(data.nopes, features.active);
});
// Targets allow hover and vertex snapping
var targets = selection.selectAll('.line.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('path')
.merge(targets)
.attr('d', getPath)
.attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; });
// NOPE
var nopes = selection.selectAll('.line.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('path')
.merge(nopes)
.attr('d', getPath)
.attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; });
}
function drawLines(selection, graph, entities, filter) {
function waystack(a, b) {
var selected = context.selectedIDs(),
scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0,
scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;
var selected = context.selectedIDs();
var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;
var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;
if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }
if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }
@@ -51,6 +102,11 @@ export function svgLines(projection, context) {
function drawLineGroup(selection, klass, isSelected) {
// Note: Don't add `.selected` class in draw modes
var mode = context.mode();
var isDrawing = mode && /^draw/.test(mode.id);
var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';
var lines = selection
.selectAll('path')
.filter(filter)
@@ -59,13 +115,13 @@ export function svgLines(projection, context) {
lines.exit()
.remove();
// Optimization: call simple TagClasses only on enter selection. This
// Optimization: Call expensive TagClasses only on enter selection. This
// works because osmEntity.key is defined to include the entity v attribute.
lines.enter()
.append('path')
.attr('class', function(d) {
return 'way line ' + klass + ' ' + d.id + (isSelected ? ' selected' : '') +
(oldMultiPolygonOuters[d.id] ? ' old-multipolygon' : '');
var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';
return 'way line ' + klass + ' ' + selectedClass + oldMPClass + d.id;
})
.call(svgTagClasses())
.merge(lines)
@@ -91,15 +147,15 @@ export function svgLines(projection, context) {
}
var getPath = svgPath(projection, graph),
ways = [],
pathdata = {},
onewaydata = {},
oldMultiPolygonOuters = {};
var getPath = svgPath(projection, graph);
var ways = [];
var pathdata = {};
var onewaydata = {};
var oldMultiPolygonOuters = {};
for (var i = 0; i < entities.length; i++) {
var entity = entities[i],
outer = osmSimpleMultipolygonOuterMember(entity, graph);
var entity = entities[i];
var outer = osmSimpleMultipolygonOuterMember(entity, graph);
if (outer) {
ways.push(entity.mergeTags(outer.tags));
oldMultiPolygonOuters[outer.id] = true;
@@ -117,7 +173,7 @@ export function svgLines(projection, context) {
});
var layer = selection.selectAll('.layer-lines');
var layer = selection.selectAll('.layer-lines .layer-lines-lines');
var layergroup = layer
.selectAll('g.layergroup')
@@ -164,8 +220,8 @@ export function svgLines(projection, context) {
.selectAll('path')
.filter(filter)
.data(
function() { return onewaydata[this.parentNode.__data__] || []; },
function(d) { return [d.id, d.index]; }
function data() { return onewaydata[this.parentNode.__data__] || []; },
function key(d) { return [d.id, d.index]; }
);
oneways.exit()
@@ -181,6 +237,11 @@ export function svgLines(projection, context) {
if (detected.ie) {
oneways.each(function() { this.parentNode.insertBefore(this, this); });
}
// touch targets
selection.selectAll('.layer-lines .layer-lines-targets')
.call(drawTargets, graph, ways, filter);
}
+11 -24
View File
@@ -1,23 +1,16 @@
import _throttle from 'lodash-es/throttle';
import {
geoIdentity as d3_geoIdentity,
geoPath as d3_geoPath
} from 'd3-geo';
import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './point_transform';
import { svgPath, svgPointTransform } from './index';
import { services } from '../services';
export function svgMapillaryImages(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000),
minZoom = 12,
minMarkerZoom = 16,
minViewfieldZoom = 18,
layer = d3_select(null),
_mapillary;
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var minMarkerZoom = 16;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
function init() {
@@ -128,25 +121,19 @@ export function svgMapillaryImages(projection, context, dispatch) {
var sequences = (service ? service.sequences(projection) : []);
var images = (service && showMarkers ? service.images(projection) : []);
var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream;
var project = projection.stream;
var makePath = d3_geoPath().projection({ stream: function(output) {
return project(clip(output));
}});
var traces = layer.selectAll('.sequences').selectAll('.sequence')
.data(sequences, function(d) { return d.properties.key; });
// exit
traces.exit()
.remove();
// enter/update
traces = traces.enter()
.append('path')
.attr('class', 'sequence')
.merge(traces);
traces
.attr('d', makePath);
.merge(traces)
.attr('d', svgPath(projection).geojson);
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
+4 -4
View File
@@ -5,10 +5,10 @@ import { services } from '../services';
export function svgMapillarySigns(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000),
minZoom = 12,
layer = d3_select(null),
_mapillary;
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _mapillary;
function init() {
+75 -24
View File
@@ -7,25 +7,68 @@ import {
import {
geoAngle,
geoEuclideanDistance,
geoInterp,
geoLineIntersection
geoLineIntersection,
geoVecInterp,
geoVecLength
} from '../geo';
export function svgMidpoints(projection, context) {
var targetRadius = 8;
return function drawMidpoints(selection, graph, entities, filter, extent) {
var layer = selection.selectAll('.layer-hit');
function drawTargets(selection, graph, entities, filter) {
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var getTransform = svgPointTransform(projection).geojson;
var data = entities.map(function(midpoint) {
return {
type: 'Feature',
id: midpoint.id,
properties: {
target: true,
entity: midpoint
},
geometry: {
type: 'Point',
coordinates: midpoint.loc
}
};
});
var targets = selection.selectAll('.midpoint.target')
.filter(function(d) { return filter(d.properties.entity); })
.data(data, function key(d) { return d.id; });
// exit
targets.exit()
.remove();
// enter/update
targets.enter()
.append('circle')
.attr('r', targetRadius)
.merge(targets)
.attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; })
.attr('transform', getTransform);
}
function drawMidpoints(selection, graph, entities, filter, extent) {
var layer = selection.selectAll('.layer-points .layer-points-midpoints');
var mode = context.mode();
if (mode && mode.id !== 'select') {
layer.selectAll('g.midpoint').remove();
layer.selectAll('g.midpoint')
.remove();
selection.selectAll('.layer-points .layer-points-targets .midpoint.target')
.remove();
return;
}
var poly = extent.polygon(),
midpoints = {};
var poly = extent.polygon();
var midpoints = {};
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
@@ -40,16 +83,16 @@ export function svgMidpoints(projection, context) {
var nodes = graph.childNodes(entity);
for (var j = 0; j < nodes.length - 1; j++) {
var a = nodes[j],
b = nodes[j + 1],
id = [a.id, b.id].sort().join('-');
var a = nodes[j];
var b = nodes[j + 1];
var id = [a.id, b.id].sort().join('-');
if (midpoints[id]) {
midpoints[id].parents.push(entity);
} else {
if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) {
var point = geoInterp(a.loc, b.loc, 0.5),
loc = null;
if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) {
var point = geoVecInterp(a.loc, b.loc, 0.5);
var loc = null;
if (extent.intersects(point)) {
loc = point;
@@ -57,8 +100,8 @@ export function svgMidpoints(projection, context) {
for (var k = 0; k < 4; k++) {
point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]);
if (point &&
geoEuclideanDistance(projection(a.loc), projection(point)) > 20 &&
geoEuclideanDistance(projection(b.loc), projection(point)) > 20)
geoVecLength(projection(a.loc), projection(point)) > 20 &&
geoVecLength(projection(b.loc), projection(point)) > 20)
{
loc = point;
break;
@@ -107,22 +150,24 @@ export function svgMidpoints(projection, context) {
.insert('g', ':first-child')
.attr('class', 'midpoint');
enter.append('polygon')
enter
.append('polygon')
.attr('points', '-6,8 10,0 -6,-8')
.attr('class', 'shadow');
enter.append('polygon')
enter
.append('polygon')
.attr('points', '-3,4 5,0 -3,-4')
.attr('class', 'fill');
groups = groups
.merge(enter)
.attr('transform', function(d) {
var translate = svgPointTransform(projection),
a = graph.entity(d.edge[0]),
b = graph.entity(d.edge[1]),
angleVal = Math.round(geoAngle(a, b, projection) * (180 / Math.PI));
return translate(d) + ' rotate(' + angleVal + ')';
var translate = svgPointTransform(projection);
var a = graph.entity(d.edge[0]);
var b = graph.entity(d.edge[1]);
var angle = geoAngle(a, b, projection) * (180 / Math.PI);
return translate(d) + ' rotate(' + angle + ')';
})
.call(svgTagClasses().tags(
function(d) { return d.parents[0].tags; }
@@ -132,5 +177,11 @@ export function svgMidpoints(projection, context) {
groups.select('polygon.shadow');
groups.select('polygon.fill');
};
// Draw touch targets..
selection.selectAll('.layer-points .layer-points-targets')
.call(drawTargets, graph, _values(midpoints), midpointFilter);
}
return drawMidpoints;
}
-67
View File
@@ -1,67 +0,0 @@
import {
geoIdentity as d3_geoIdentity,
geoStream as d3_geoStream
} from 'd3-geo';
import { geoEuclideanDistance } from '../geo';
export function svgOneWaySegments(projection, graph, dt) {
return function(entity) {
var a,
b,
i = 0,
offset = dt,
segments = [],
clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream,
coordinates = graph.childNodes(entity).map(function(n) {
return n.loc;
});
if (entity.tags.oneway === '-1') coordinates.reverse();
d3_geoStream({
type: 'LineString',
coordinates: coordinates
}, projection.stream(clip({
lineStart: function() {},
lineEnd: function() {
a = null;
},
point: function(x, y) {
b = [x, y];
if (a) {
var span = geoEuclideanDistance(a, b) - offset;
if (span >= 0) {
var angle = Math.atan2(b[1] - a[1], b[0] - a[0]),
dx = dt * Math.cos(angle),
dy = dt * Math.sin(angle),
p = [a[0] + offset * Math.cos(angle),
a[1] + offset * Math.sin(angle)];
var segment = 'M' + a[0] + ',' + a[1] +
'L' + p[0] + ',' + p[1];
for (span -= dt; span >= 0; span -= dt) {
p[0] += dx;
p[1] += dy;
segment += 'L' + p[0] + ',' + p[1];
}
segment += 'L' + b[0] + ',' + b[1];
segments.push({id: entity.id, index: i, d: segment});
}
offset = -span;
i++;
}
a = b;
}
})));
return segments;
};
}
+11 -24
View File
@@ -1,23 +1,16 @@
import _throttle from 'lodash-es/throttle';
import {
geoIdentity as d3_geoIdentity,
geoPath as d3_geoPath
} from 'd3-geo';
import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './point_transform';
import { svgPath, svgPointTransform } from './index';
import { services } from '../services';
export function svgOpenstreetcamImages(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000),
minZoom = 12,
minMarkerZoom = 16,
minViewfieldZoom = 18,
layer = d3_select(null),
_openstreetcam;
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var minMarkerZoom = 16;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _openstreetcam;
function init() {
@@ -128,25 +121,19 @@ export function svgOpenstreetcamImages(projection, context, dispatch) {
var sequences = (service ? service.sequences(projection) : []);
var images = (service && showMarkers ? service.images(projection) : []);
var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream;
var project = projection.stream;
var makePath = d3_geoPath().projection({ stream: function(output) {
return project(clip(output));
}});
var traces = layer.selectAll('.sequences').selectAll('.sequence')
.data(sequences, function(d) { return d.properties.key; });
// exit
traces.exit()
.remove();
// enter/update
traces = traces.enter()
.append('path')
.attr('class', 'sequence')
.merge(traces);
traces
.attr('d', makePath);
.merge(traces)
.attr('d', svgPath(projection).geojson);
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
+25 -1
View File
@@ -4,10 +4,34 @@ export function svgOsm(projection, context, dispatch) {
function drawOsm(selection) {
selection.selectAll('.layer-osm')
.data(['areas', 'lines', 'hit', 'halo', 'label'])
.data(['areas', 'lines', 'points', 'labels'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-osm layer-' + d; });
selection.selectAll('.layer-areas').selectAll('.layer-areas-group')
.data(['areas', 'targets'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-areas-group layer-areas-' + d; });
selection.selectAll('.layer-lines').selectAll('.layer-lines-group')
.data(['lines', 'targets'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-lines-group layer-lines-' + d; });
selection.selectAll('.layer-points').selectAll('.layer-points-group')
.data(['points', 'midpoints', 'vertices', 'turns', 'targets'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-points-group layer-points-' + d; });
selection.selectAll('.layer-labels').selectAll('.layer-labels-group')
.data(['halo', 'label', 'debug'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; });
}
-37
View File
@@ -1,37 +0,0 @@
import {
geoIdentity as d3_geoIdentity,
geoPath as d3_geoPath
} from 'd3-geo';
export function svgPath(projection, graph, isArea) {
// Explanation of magic numbers:
// "padding" here allows space for strokes to extend beyond the viewport,
// so that the stroke isn't drawn along the edge of the viewport when
// the shape is clipped.
//
// When drawing lines, pad viewport by 5px.
// When drawing areas, pad viewport by 65px in each direction to allow
// for 60px area fill stroke (see ".fill-partial path.fill" css rule)
var cache = {},
padding = isArea ? 65 : 5,
viewport = projection.clipExtent(),
paddedExtent = [
[viewport[0][0] - padding, viewport[0][1] - padding],
[viewport[1][0] + padding, viewport[1][1] + padding]
],
clip = d3_geoIdentity().clipExtent(paddedExtent).stream,
project = projection.stream,
path = d3_geoPath()
.projection({stream: function(output) { return project(clip(output)); }});
return function(entity) {
if (entity.id in cache) {
return cache[entity.id];
} else {
return cache[entity.id] = path(entity.asGeoJSON(graph));
}
};
}
-7
View File
@@ -1,7 +0,0 @@
export function svgPointTransform(projection) {
return function(entity) {
// http://jsperf.com/short-array-join
var pt = projection(entity.loc);
return 'translate(' + pt[0] + ',' + pt[1] + ')';
};
}
+84 -15
View File
@@ -1,6 +1,5 @@
import _filter from 'lodash-es/filter';
import { dataFeatureIcons } from '../../data';
import { geoScaleToZoom } from '../geo';
import { osmEntity } from '../osm';
import { svgPointTransform, svgTagClasses } from './index';
@@ -19,19 +18,77 @@ export function svgPoints(projection, context) {
}
return function drawPoints(selection, graph, entities, filter) {
var wireframe = context.surface().classed('fill-wireframe'),
points = wireframe ? [] : _filter(entities, function(e) {
return e.geometry(graph) === 'point';
// 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 drawTargets(selection, graph, entities, filter) {
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var getTransform = svgPointTransform(projection).geojson;
var activeID = context.activeID();
var data = [];
entities.forEach(function(node) {
if (activeID === node.id) return; // draw no target on the activeID
data.push({
type: 'Feature',
id: node.id,
properties: {
target: true,
entity: node
},
geometry: node.asGeoJSON()
});
});
var targets = selection.selectAll('.point.target')
.filter(function(d) { return filter(d.properties.entity); })
.data(data, function key(d) { return d.id; });
// exit
targets.exit()
.remove();
// enter/update
targets.enter()
.append('rect')
.attr('x', -10)
.attr('y', -26)
.attr('width', 20)
.attr('height', 30)
.merge(targets)
.attr('class', function(d) { return 'node point target ' + fillClass + d.id; })
.attr('transform', getTransform);
}
function drawPoints(selection, graph, entities, filter) {
var wireframe = context.surface().classed('fill-wireframe');
var zoom = geoScaleToZoom(projection.scale());
// points with a direction will render as vertices at higher zooms
function renderAsPoint(entity) {
return entity.geometry(graph) === 'point' &&
!(zoom >= 18 && entity.directions(graph, projection).length);
}
// all points will render as vertices in wireframe mode too
var points = wireframe ? [] : entities.filter(renderAsPoint);
points.sort(sortY);
var layer = selection.selectAll('.layer-hit');
var layer = selection.selectAll('.layer-points .layer-points-points');
var groups = layer.selectAll('g.point')
.filter(filter)
.data(points, osmEntity.key);
.data(points, fastEntityKey);
groups.exit()
.remove();
@@ -41,20 +98,24 @@ export function svgPoints(projection, context) {
.attr('class', function(d) { return 'node point ' + d.id; })
.order();
enter.append('path')
enter
.append('path')
.call(markerPath, 'shadow');
enter.append('ellipse')
enter
.append('ellipse')
.attr('cx', 0.5)
.attr('cy', 1)
.attr('rx', 6.5)
.attr('ry', 3)
.attr('class', 'stroke');
enter.append('path')
enter
.append('path')
.call(markerPath, 'stroke');
enter.append('use')
enter
.append('use')
.attr('transform', 'translate(-5, -19)')
.attr('class', 'icon')
.attr('width', '11px')
@@ -71,8 +132,8 @@ export function svgPoints(projection, context) {
groups.select('.stroke');
groups.select('.icon')
.attr('xlink:href', function(entity) {
var preset = context.presets().match(entity, graph),
picon = preset && preset.icon;
var preset = context.presets().match(entity, graph);
var picon = preset && preset.icon;
if (!picon)
return '';
@@ -81,5 +142,13 @@ export function svgPoints(projection, context) {
return '#' + picon + (isMaki ? '-11' : '');
}
});
};
// touch targets
selection.selectAll('.layer-points .layer-points-targets')
.call(drawTargets, graph, points, filter);
}
return drawPoints;
}
-15
View File
@@ -1,15 +0,0 @@
import _extend from 'lodash-es/extend';
export function svgRelationMemberTags(graph) {
return function(entity) {
var tags = entity.tags;
graph.parentRelations(entity).forEach(function(relation) {
var type = relation.tags.type;
if (type === 'multipolygon' || type === 'boundary') {
tags = _extend({}, relation.tags, tags);
}
});
return tags;
};
}
+2 -1
View File
@@ -18,7 +18,8 @@ export function svgTurns(projection) {
(!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u;
}
var groups = selection.selectAll('.layer-hit').selectAll('g.turn')
var layer = selection.selectAll('.layer-points .layer-points-turns');
var groups = layer.selectAll('g.turn')
.data(turns, key);
groups.exit()
+337 -123
View File
@@ -1,208 +1,422 @@
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 { svgPointTransform } from './index';
import { svgPassiveVertex, svgPointTransform } from './index';
export function svgVertices(projection, context) {
var radiuses = {
// z16-, z17, z18+, tagged
shadow: [6, 7.5, 7.5, 11.5],
stroke: [2.5, 3.5, 3.5, 7],
fill: [1, 1.5, 1.5, 1.5]
// 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 hover;
var _currHoverTarget;
var _currPersistent = {};
var _currHover = {};
var _prevHover = {};
var _currSelected = {};
var _prevSelected = {};
var _radii = {};
function siblingAndChildVertices(ids, graph, extent) {
var vertices = {};
function sortY(a, b) {
return b.loc[1] - a.loc[1];
}
function addChildVertices(entity) {
if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) {
var i;
if (entity.type === 'way') {
for (i = 0; i < entity.nodes.length; i++) {
addChildVertices(graph.entity(entity.nodes[i]));
}
} else if (entity.type === 'relation') {
for (i = 0; i < entity.members.length; i++) {
var member = context.hasEntity(entity.members[i].id);
if (member) {
addChildVertices(member);
}
}
} else if (entity.intersects(extent, graph)) {
vertices[entity.id] = entity;
}
}
}
ids.forEach(function(id) {
var entity = context.hasEntity(id);
if (entity && entity.type === 'node') {
vertices[entity.id] = entity;
context.graph().parentWays(entity).forEach(function(entity) {
addChildVertices(entity);
});
} else if (entity) {
addChildVertices(entity);
}
});
return vertices;
// 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, vertices, klass, graph, zoom, siblings) {
function draw(selection, graph, vertices, sets, filter) {
sets = sets || { selected: {}, important: {}, hovered: {} };
function icon(entity) {
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];
}
function setClass(klass) {
return function(entity) {
this.setAttribute('class', 'node vertex ' + klass + ' ' + 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 setAttributes(selection) {
['shadow','stroke','fill'].forEach(function(klass) {
function updateAttributes(selection) {
['shadow', 'stroke', 'fill'].forEach(function(klass) {
var rads = radiuses[klass];
selection.selectAll('.' + klass)
.each(function(entity) {
var i = z && icon(entity),
c = i ? 0.5 : 0,
r = rads[i ? 3 : z];
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;
}
this.setAttribute('cx', c);
this.setAttribute('cy', -c);
this.setAttribute('r', r);
if (i && klass === 'fill') {
this.setAttribute('visibility', 'hidden');
} else {
this.removeAttribute('visibility');
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')
.each(function() {
if (z) {
this.removeAttribute('visibility');
} else {
this.setAttribute('visibility', 'hidden');
}
});
.attr('visibility', (z === 0 ? 'hidden' : null));
}
vertices.sort(sortY);
siblings = siblings || {};
var icons = {},
z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);
var groups = selection
.data(vertices, osmEntity.key);
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 ' + klass + ' ' + d.id; });
.attr('class', function(d) { return 'node vertex ' + d.id; })
.order();
enter.append('circle')
.each(setClass('shadow'));
enter
.append('circle')
.attr('class', 'shadow');
enter.append('circle')
.each(setClass('stroke'));
enter
.append('circle')
.attr('class', 'stroke');
// Vertices with icons get a `use`.
enter.filter(function(d) { return icon(d); })
enter.filter(function(d) { return getIcon(d); })
.append('use')
.attr('transform', 'translate(-5, -6)')
.attr('xlink:href', function(d) {
var picon = icon(d),
isMaki = dataFeatureIcons.indexOf(picon) !== -1;
return '#' + picon + (isMaki ? '-11' : '');
})
.attr('class', 'icon')
.attr('width', '11px')
.attr('height', '11px')
.each(setClass('icon'));
.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')
.each(setClass('fill'));
.attr('class', 'fill');
groups
// update
groups = groups
.merge(enter)
.attr('transform', svgPointTransform(projection))
.classed('sibling', function(entity) { return entity.id in siblings; })
.classed('shared', function(entity) { return graph.isShared(entity); })
.classed('endpoint', function(entity) { return entity.isEndpoint(graph); })
.call(setAttributes);
.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 drawVertices(selection, graph, entities, filter, extent, zoom) {
var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent),
wireframe = context.surface().classed('fill-wireframe'),
vertices = [];
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: [] };
for (var i = 0; i < entities.length; i++) {
var entity = entities[i],
geometry = entity.geometry(graph);
entities.forEach(function(node) {
if (activeID === node.id) return; // draw no target on the activeID
if (wireframe && geometry === 'point') {
vertices.push(entity);
continue;
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', // break the ids on purpose
properties: {
target: true,
entity: node,
nope: true,
originalID: node.id
},
geometry: node.asGeoJSON()
});
}
});
if (geometry !== 'vertex')
continue;
if (entity.id in siblings ||
entity.hasInterestingTags() ||
entity.isEndpoint(graph) ||
entity.isConnected(graph)) {
vertices.push(entity);
// 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.originalID] || 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;
}
}
}
var layer = selection.selectAll('.layer-hit');
layer.selectAll('g.vertex.vertex-persistent')
.filter(filter)
.call(draw, vertices, 'vertex-persistent', graph, zoom, siblings);
ids.forEach(function(id) {
var entity = graph.hasEntity(id);
if (!entity) return;
drawHover(selection, graph, extent, zoom);
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 drawHover(selection, graph, extent, zoom) {
var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {};
var layer = selection.selectAll('.layer-hit');
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);
layer.selectAll('g.vertex.vertex-hover')
.call(draw, _values(hovered), 'vertex-hover', graph, zoom);
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); });
}
}
drawVertices.drawHover = function(selection, graph, target, extent, zoom) {
if (target === hover) return;
hover = target;
drawHover(selection, graph, extent, zoom);
// 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;
+2 -2
View File
@@ -3,7 +3,7 @@ import {
select as d3_select
} from 'd3-selection';
import { geoRoundCoords } from '../geo';
import { geoVecFloor } from '../geo';
import { textDirection } from '../util/locale';
import { uiTooltipHtml } from './tooltipHtml';
@@ -81,7 +81,7 @@ export function uiEditMenu(context, operations) {
.attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })
.classed('disabled', function (d) { return d.disabled(); })
.attr('transform', function (d, i) {
return 'translate(' + geoRoundCoords([
return 'translate(' + geoVecFloor([
0,
m + i * buttonHeight
]).join(',') + ')';
+31 -25
View File
@@ -26,7 +26,8 @@ import {
import {
geoExtent,
geoRawMercator
geoRawMercator,
geoZoomToScale
} from '../../geo';
import {
@@ -46,12 +47,12 @@ import {
export function uiFieldRestrictions(field, context) {
var dispatch = d3_dispatch('change'),
breathe = behaviorBreathe(context),
hover = behaviorHover(context),
initialized = false,
vertexID,
fromNodeID;
var dispatch = d3_dispatch('change');
var breathe = behaviorBreathe(context);
var hover = behaviorHover(context);
var initialized = false;
var vertexID;
var fromNodeID;
function restrictions(selection) {
@@ -73,19 +74,18 @@ export function uiFieldRestrictions(field, context) {
.attr('class', 'restriction-help');
var intersection = osmIntersection(context.graph(), vertexID),
graph = intersection.graph,
vertex = graph.entity(vertexID),
filter = utilFunctor(true),
extent = geoExtent(),
projection = geoRawMercator();
var intersection = osmIntersection(context.graph(), vertexID);
var graph = intersection.graph;
var vertex = graph.entity(vertexID);
var filter = utilFunctor(true);
var projection = geoRawMercator();
var d = utilGetDimensions(wrap.merge(enter)),
c = [d[0] / 2, d[1] / 2],
z = 24;
var d = utilGetDimensions(wrap.merge(enter));
var c = [d[0] / 2, d[1] / 2];
var z = 24;
projection
.scale(256 * Math.pow(2, z) / (2 * Math.PI));
.scale(geoZoomToScale(z));
var s = projection(vertex.loc);
@@ -93,10 +93,12 @@ export function uiFieldRestrictions(field, context) {
.translate([c[0] - s[0], c[1] - s[1]])
.clipExtent([[0, 0], d]);
var drawLayers = svgLayers(projection, context).only('osm').dimensions(d),
drawVertices = svgVertices(projection, context),
drawLines = svgLines(projection, context),
drawTurns = svgTurns(projection, context);
var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0]));
var drawLayers = svgLayers(projection, context).only('osm').dimensions(d);
var drawVertices = svgVertices(projection, context);
var drawLines = svgLines(projection, context);
var drawTurns = svgTurns(projection, context);
enter
.call(drawLayers);
@@ -115,7 +117,7 @@ export function uiFieldRestrictions(field, context) {
surface
.call(utilSetDimensions, d)
.call(drawVertices, graph, [vertex], filter, extent, z)
.call(drawVertices, graph, [vertex], filter, extent, true)
.call(drawLines, graph, intersection.ways, filter)
.call(drawTurns, graph, intersection.turns(fromNodeID));
@@ -152,9 +154,13 @@ export function uiFieldRestrictions(field, context) {
.call(breathe);
var datum = d3_event.target.__data__;
var entity = datum && datum.properties && datum.properties.entity;
if (entity) datum = entity;
if (datum instanceof osmEntity) {
fromNodeID = intersection.adjacentNodeId(datum.id);
render();
} else if (datum instanceof osmTurn) {
if (datum.restriction) {
context.perform(
@@ -174,9 +180,9 @@ export function uiFieldRestrictions(field, context) {
function mouseover() {
var datum = d3_event.target.__data__;
if (datum instanceof osmTurn) {
var graph = context.graph(),
presets = context.presets(),
preset;
var graph = context.graph();
var presets = context.presets();
var preset;
if (datum.restriction) {
preset = presets.match(graph.entity(datum.restriction), graph);
+59 -60
View File
@@ -13,45 +13,44 @@ import {
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { t } from '../util/locale';
import { svgDebug, svgGpx } from '../svg';
import { geoRawMercator } from '../geo';
import {
geoRawMercator,
geoScaleToZoom,
geoVecSubtract,
geoVecScale,
geoZoomToScale,
} from '../geo';
import { rendererTileLayer } from '../renderer';
import { svgDebug, svgGpx } from '../svg';
import { utilSetTransform } from '../util';
import { utilGetDimensions } from '../util/dimensions';
var TAU = 2 * Math.PI;
function ztok(z) { return 256 * Math.pow(2, z) / TAU; }
function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; }
function vecSub(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; }
function vecScale(a, b) { return [ a[0] * b, a[1] * b ]; }
export function uiMapInMap(context) {
function map_in_map(selection) {
var backgroundLayer = rendererTileLayer(context),
overlayLayers = {},
projection = geoRawMercator(),
gpxLayer = svgGpx(projection, context).showLabels(false),
debugLayer = svgDebug(projection, context),
zoom = d3_zoom()
.scaleExtent([ztok(0.5), ztok(24)])
.on('start', zoomStarted)
.on('zoom', zoomed)
.on('end', zoomEnded),
isTransformed = false,
isHidden = true,
skipEvents = false,
gesture = null,
zDiff = 6, // by default, minimap renders at (main zoom - 6)
wrap = d3_select(null),
tiles = d3_select(null),
viewport = d3_select(null),
tStart, // transform at start of gesture
tCurr, // transform at most recent event
timeoutId;
var backgroundLayer = rendererTileLayer(context);
var overlayLayers = {};
var projection = geoRawMercator();
var gpxLayer = svgGpx(projection, context).showLabels(false);
var debugLayer = svgDebug(projection, context);
var zoom = d3_zoom()
.scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])
.on('start', zoomStarted)
.on('zoom', zoomed)
.on('end', zoomEnded);
var isTransformed = false;
var isHidden = true;
var skipEvents = false;
var gesture = null;
var zDiff = 6; // by default, minimap renders at (main zoom - 6)
var wrap = d3_select(null);
var tiles = d3_select(null);
var viewport = d3_select(null);
var tStart; // transform at start of gesture
var tCurr; // transform at most recent event
var timeoutId;
function zoomStarted() {
@@ -64,11 +63,11 @@ export function uiMapInMap(context) {
function zoomed() {
if (skipEvents) return;
var x = d3_event.transform.x,
y = d3_event.transform.y,
k = d3_event.transform.k,
isZooming = (k !== tStart.k),
isPanning = (x !== tStart.x || y !== tStart.y);
var x = d3_event.transform.x;
var y = d3_event.transform.y;
var k = d3_event.transform.k;
var isZooming = (k !== tStart.k);
var isPanning = (x !== tStart.x || y !== tStart.y);
if (!isZooming && !isPanning) {
return; // no change
@@ -79,12 +78,12 @@ export function uiMapInMap(context) {
gesture = isZooming ? 'zoom' : 'pan';
}
var tMini = projection.transform(),
tX, tY, scale;
var tMini = projection.transform();
var tX, tY, scale;
if (gesture === 'zoom') {
var dMini = utilGetDimensions(wrap),
cMini = vecScale(dMini, 0.5);
var dMini = utilGetDimensions(wrap);
var cMini = geoVecScale(dMini, 0.5);
scale = k / tMini.k;
tX = (cMini[0] / scale - cMini[0]) * scale;
tY = (cMini[1] / scale - cMini[1]) * scale;
@@ -100,8 +99,8 @@ export function uiMapInMap(context) {
isTransformed = true;
tCurr = d3_zoomIdentity.translate(x, y).scale(k);
var zMain = ktoz(context.projection.scale()),
zMini = ktoz(k);
var zMain = geoScaleToZoom(context.projection.scale());
var zMini = geoScaleToZoom(k);
zDiff = zMain - zMini;
@@ -115,29 +114,29 @@ export function uiMapInMap(context) {
updateProjection();
gesture = null;
var dMini = utilGetDimensions(wrap),
cMini = vecScale(dMini, 0.5);
var dMini = utilGetDimensions(wrap);
var cMini = geoVecScale(dMini, 0.5);
context.map().center(projection.invert(cMini)); // recenter main map..
}
function updateProjection() {
var loc = context.map().center(),
dMini = utilGetDimensions(wrap),
cMini = vecScale(dMini, 0.5),
tMain = context.projection.transform(),
zMain = ktoz(tMain.k),
zMini = Math.max(zMain - zDiff, 0.5),
kMini = ztok(zMini);
var loc = context.map().center();
var dMini = utilGetDimensions(wrap);
var cMini = geoVecScale(dMini, 0.5);
var tMain = context.projection.transform();
var zMain = geoScaleToZoom(tMain.k);
var zMini = Math.max(zMain - zDiff, 0.5);
var kMini = geoZoomToScale(zMini);
projection
.translate([tMain.x, tMain.y])
.scale(kMini);
var point = projection(loc),
mouse = (gesture === 'pan') ? vecSub([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0],
xMini = cMini[0] - point[0] + tMain.x + mouse[0],
yMini = cMini[1] - point[1] + tMain.y + mouse[1];
var point = projection(loc);
var mouse = (gesture === 'pan') ? geoVecSubtract([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0];
var xMini = cMini[0] - point[0] + tMain.x + mouse[0];
var yMini = cMini[1] - point[1] + tMain.y + mouse[1];
projection
.translate([xMini, yMini])
@@ -152,7 +151,7 @@ export function uiMapInMap(context) {
}
zoom
.scaleExtent([ztok(0.5), ztok(zMain - 3)]);
.scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]);
skipEvents = true;
wrap.call(zoom.transform, tCurr);
@@ -166,8 +165,8 @@ export function uiMapInMap(context) {
updateProjection();
var dMini = utilGetDimensions(wrap),
zMini = ktoz(projection.scale());
var dMini = utilGetDimensions(wrap);
var zMini = geoScaleToZoom(projection.scale());
// setup tile container
tiles = wrap
@@ -249,8 +248,8 @@ export function uiMapInMap(context) {
// redraw viewport bounding box
if (gesture !== 'pan') {
var getPath = d3_geoPath(projection),
bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };
var getPath = d3_geoPath(projection);
var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };
viewport = wrap.selectAll('.map-in-map-viewport')
.data([0]);
+2 -2
View File
@@ -3,7 +3,7 @@ import {
select as d3_select
} from 'd3-selection';
import { geoRoundCoords } from '../geo';
import { geoVecFloor } from '../geo';
import { uiTooltipHtml } from './tooltipHtml';
@@ -58,7 +58,7 @@ export function uiRadialMenu(context, operations) {
.attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; })
.classed('disabled', function(d) { return d.disabled(); })
.attr('transform', function(d, i) {
return 'translate(' + geoRoundCoords([
return 'translate(' + geoVecFloor([
r * Math.sin(a0 + i * a),
r * Math.cos(a0 + i * a)]).join(',') + ')';
});