mirror of
https://github.com/FoggedLens/iD.git
synced 2026-06-03 13:38:04 +02:00
Merge pull request #4602 from openstreetmap/direction_vertices
Support rendering `direction` on vertices (stop sign, traffic_signals, etc)
This commit is contained in:
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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')));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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; });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(',') + ')';
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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(',') + ')';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user