mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-25 01:24:05 +02:00
Support straightening of points
(closes #6217) - Split `actionStraighten` into `actionStraightenWay` and `actionStraightenNodes` - Now `operationStraighten` chooses the correct action depending on selected entities - Also move `getSmallestSurroundingRectangle` from `actionReflect` to `geo.js`
This commit is contained in:
@@ -31,7 +31,8 @@ export { actionReverse } from './reverse';
|
||||
export { actionRevert } from './revert';
|
||||
export { actionRotate } from './rotate';
|
||||
export { actionSplit } from './split';
|
||||
export { actionStraighten } from './straighten';
|
||||
export { actionStraightenNodes } from './straighten_nodes';
|
||||
export { actionStraightenWay } from './straighten_way';
|
||||
export { actionUnrestrictTurn } from './unrestrict_turn';
|
||||
export { actionReflect } from './reflect.js';
|
||||
export { actionUpgradeTags } from './upgrade_tags';
|
||||
|
||||
@@ -1,50 +1,10 @@
|
||||
import {
|
||||
polygonHull as d3_polygonHull,
|
||||
polygonCentroid as d3_polygonCentroid
|
||||
} from 'd3-polygon';
|
||||
|
||||
import { geoExtent, geoRotate, geoVecInterp, geoVecLength } from '../geo';
|
||||
import { geoGetSmallestSurroundingRectangle, geoVecInterp, geoVecLength } from '../geo';
|
||||
import { utilGetAllNodes } from '../util';
|
||||
|
||||
|
||||
/* Reflect the given area around its axis of symmetry */
|
||||
export function actionReflect(reflectIds, projection) {
|
||||
var useLongAxis = true;
|
||||
|
||||
|
||||
// http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points
|
||||
// http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756
|
||||
function getSmallestSurroundingRectangle(graph, nodes) {
|
||||
var points = nodes.map(function(n) { return projection(n.loc); });
|
||||
var hull = d3_polygonHull(points);
|
||||
var centroid = d3_polygonCentroid(hull);
|
||||
var minArea = Infinity;
|
||||
var ssrExtent = [];
|
||||
var ssrAngle = 0;
|
||||
var c1 = hull[0];
|
||||
|
||||
for (var i = 0; i <= hull.length - 1; i++) {
|
||||
var c2 = (i === hull.length - 1) ? hull[0] : hull[i + 1];
|
||||
var angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]);
|
||||
var poly = geoRotate(hull, -angle, centroid);
|
||||
var extent = poly.reduce(function(extent, point) {
|
||||
return extent.extend(geoExtent(point));
|
||||
}, geoExtent());
|
||||
|
||||
var area = extent.area();
|
||||
if (area < minArea) {
|
||||
minArea = area;
|
||||
ssrExtent = extent;
|
||||
ssrAngle = angle;
|
||||
}
|
||||
c1 = c2;
|
||||
}
|
||||
|
||||
return {
|
||||
poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid),
|
||||
angle: ssrAngle
|
||||
};
|
||||
}
|
||||
var _useLongAxis = true;
|
||||
|
||||
|
||||
var action = function(graph, t) {
|
||||
@@ -52,7 +12,8 @@ export function actionReflect(reflectIds, projection) {
|
||||
t = Math.min(Math.max(+t, 0), 1);
|
||||
|
||||
var nodes = utilGetAllNodes(reflectIds, graph);
|
||||
var ssr = getSmallestSurroundingRectangle(graph, nodes);
|
||||
var points = nodes.map(function(n) { return projection(n.loc); });
|
||||
var ssr = geoGetSmallestSurroundingRectangle(points);
|
||||
|
||||
// Choose line pq = axis of symmetry.
|
||||
// The shape's surrounding rectangle has 2 axes of symmetry.
|
||||
@@ -64,7 +25,7 @@ export function actionReflect(reflectIds, projection) {
|
||||
var p, q;
|
||||
|
||||
var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2));
|
||||
if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) {
|
||||
if ((_useLongAxis && isLong) || (!_useLongAxis && !isLong)) {
|
||||
p = p1;
|
||||
q = q1;
|
||||
} else {
|
||||
@@ -95,8 +56,8 @@ export function actionReflect(reflectIds, projection) {
|
||||
|
||||
|
||||
action.useLongAxis = function(val) {
|
||||
if (!arguments.length) return useLongAxis;
|
||||
useLongAxis = val;
|
||||
if (!arguments.length) return _useLongAxis;
|
||||
_useLongAxis = val;
|
||||
return action;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { geoGetSmallestSurroundingRectangle, geoVecDot, geoVecLength, geoVecInterp } from '../geo';
|
||||
|
||||
|
||||
/* Align nodes along their common axis */
|
||||
export function actionStraightenNodes(nodeIDs, projection) {
|
||||
|
||||
function positionAlongWay(a, o, b) {
|
||||
return geoVecDot(a, b, o) / geoVecDot(b, b, o);
|
||||
}
|
||||
|
||||
|
||||
var action = function(graph, t) {
|
||||
if (t === null || !isFinite(t)) t = 1;
|
||||
t = Math.min(Math.max(+t, 0), 1);
|
||||
|
||||
var nodes = nodeIDs.map(function(id) { return graph.entity(id); });
|
||||
var points = nodes.map(function(n) { return projection(n.loc); });
|
||||
var ssr = geoGetSmallestSurroundingRectangle(points);
|
||||
|
||||
// Choose line pq = axis of symmetry.
|
||||
// The shape's surrounding rectangle has 2 axes of symmetry.
|
||||
// Snap points to the long axis
|
||||
var p1 = [(ssr.poly[0][0] + ssr.poly[1][0]) / 2, (ssr.poly[0][1] + ssr.poly[1][1]) / 2 ];
|
||||
var q1 = [(ssr.poly[2][0] + ssr.poly[3][0]) / 2, (ssr.poly[2][1] + ssr.poly[3][1]) / 2 ];
|
||||
var p2 = [(ssr.poly[3][0] + ssr.poly[4][0]) / 2, (ssr.poly[3][1] + ssr.poly[4][1]) / 2 ];
|
||||
var q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ];
|
||||
var p, q;
|
||||
|
||||
var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2));
|
||||
if (isLong) {
|
||||
p = p1;
|
||||
q = q1;
|
||||
} else {
|
||||
p = p2;
|
||||
q = q2;
|
||||
}
|
||||
|
||||
// Move points onto line pq
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
var node = nodes[i];
|
||||
var point = points[i];
|
||||
var u = positionAlongWay(point, p, q);
|
||||
var point2 = geoVecInterp(p, q, u);
|
||||
var loc2 = projection.invert(point2);
|
||||
graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t)));
|
||||
}
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
|
||||
action.disabled = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
action.transitionable = true;
|
||||
|
||||
|
||||
return action;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { utilArrayDifference } from '../util';
|
||||
/*
|
||||
* Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as
|
||||
*/
|
||||
export function actionStraighten(selectedIDs, projection) {
|
||||
export function actionStraightenWay(selectedIDs, projection) {
|
||||
|
||||
function positionAlongWay(a, o, b) {
|
||||
return geoVecDot(a, b, o) / geoVecDot(b, b, o);
|
||||
+45
-7
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
geoVecAngle,
|
||||
geoVecCross,
|
||||
geoVecDot,
|
||||
geoVecEqual,
|
||||
geoVecInterp,
|
||||
geoVecLength,
|
||||
geoVecSubtract
|
||||
polygonHull as d3_polygonHull,
|
||||
polygonCentroid as d3_polygonCentroid
|
||||
} from 'd3-polygon';
|
||||
|
||||
import { geoExtent } from './extent.js';
|
||||
|
||||
import {
|
||||
geoVecAngle, geoVecCross, geoVecDot, geoVecEqual,
|
||||
geoVecInterp, geoVecLength, geoVecSubtract
|
||||
} from './vector.js';
|
||||
|
||||
|
||||
@@ -15,11 +17,13 @@ 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) {
|
||||
@@ -272,6 +276,40 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) {
|
||||
}
|
||||
|
||||
|
||||
// http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points
|
||||
// http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756
|
||||
export function geoGetSmallestSurroundingRectangle(points) {
|
||||
var hull = d3_polygonHull(points);
|
||||
var centroid = d3_polygonCentroid(hull);
|
||||
var minArea = Infinity;
|
||||
var ssrExtent = [];
|
||||
var ssrAngle = 0;
|
||||
var c1 = hull[0];
|
||||
|
||||
for (var i = 0; i <= hull.length - 1; i++) {
|
||||
var c2 = (i === hull.length - 1) ? hull[0] : hull[i + 1];
|
||||
var angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]);
|
||||
var poly = geoRotate(hull, -angle, centroid);
|
||||
var extent = poly.reduce(function(extent, point) {
|
||||
return extent.extend(geoExtent(point));
|
||||
}, geoExtent());
|
||||
|
||||
var area = extent.area();
|
||||
if (area < minArea) {
|
||||
minArea = area;
|
||||
ssrExtent = extent;
|
||||
ssrAngle = angle;
|
||||
}
|
||||
c1 = c2;
|
||||
}
|
||||
|
||||
return {
|
||||
poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid),
|
||||
angle: ssrAngle
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function geoPathLength(path) {
|
||||
var length = 0;
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
|
||||
@@ -14,6 +14,7 @@ export { geoZoomToScale } from './geo.js';
|
||||
export { geoAngle } from './geom.js';
|
||||
export { geoChooseEdge } from './geom.js';
|
||||
export { geoEdgeEqual } from './geom.js';
|
||||
export { geoGetSmallestSurroundingRectangle } from './geom.js';
|
||||
export { geoHasLineIntersections } from './geom.js';
|
||||
export { geoHasSelfIntersections } from './geom.js';
|
||||
export { geoRotate } from './geom.js';
|
||||
|
||||
@@ -1,65 +1,81 @@
|
||||
import { t } from '../util/locale';
|
||||
import { actionStraighten } from '../actions/index';
|
||||
import { actionStraightenNodes, actionStraightenWay } from '../actions/index';
|
||||
import { behaviorOperation } from '../behavior/index';
|
||||
import { utilArrayDifference, utilGetAllNodes } from '../util/index';
|
||||
|
||||
|
||||
export function operationStraighten(selectedIDs, context) {
|
||||
var action = actionStraighten(selectedIDs, context.projection);
|
||||
var wayIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'w'; });
|
||||
var nodes = utilGetAllNodes(wayIDs, context.graph());
|
||||
var nodeIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'n'; });
|
||||
|
||||
var nodes = utilGetAllNodes(selectedIDs, context.graph());
|
||||
var coords = nodes.map(function(n) { return n.loc; });
|
||||
var action = chooseAction();
|
||||
var geometry;
|
||||
var _disabled;
|
||||
|
||||
|
||||
function chooseAction() {
|
||||
// straighten selected nodes
|
||||
if (wayIDs.length === 0 && nodeIDs.length > 2) {
|
||||
geometry = 'points';
|
||||
return actionStraightenNodes(nodeIDs, context.projection);
|
||||
|
||||
// straighten selected ways (possibly between range of 2 selected nodes)
|
||||
} else if (wayIDs.length > 0 && (nodeIDs.length === 0 || nodeIDs.length === 2)) {
|
||||
var startNodeIDs = [];
|
||||
var endNodeIDs = [];
|
||||
|
||||
for (var i = 0; i < selectedIDs.length; i++) {
|
||||
var entity = context.entity(selectedIDs[i]);
|
||||
if (entity.type === 'node') {
|
||||
continue;
|
||||
} else if (entity.type !== 'way' || entity.isClosed()) {
|
||||
return false; // exit early, can't straighten these
|
||||
}
|
||||
|
||||
startNodeIDs.push(entity.first());
|
||||
endNodeIDs.push(entity.last());
|
||||
}
|
||||
|
||||
// Remove duplicate end/startNodeIDs (duplicate nodes cannot be at the line end)
|
||||
startNodeIDs = startNodeIDs.filter(function(n) {
|
||||
return startNodeIDs.indexOf(n) === startNodeIDs.lastIndexOf(n);
|
||||
});
|
||||
endNodeIDs = endNodeIDs.filter(function(n) {
|
||||
return endNodeIDs.indexOf(n) === endNodeIDs.lastIndexOf(n);
|
||||
});
|
||||
|
||||
// Ensure all ways are connected (i.e. only 2 unique endpoints/startpoints)
|
||||
if (utilArrayDifference(startNodeIDs, endNodeIDs).length +
|
||||
utilArrayDifference(endNodeIDs, startNodeIDs).length !== 2) return false;
|
||||
|
||||
// Ensure path contains at least 3 unique nodes
|
||||
var wayNodeIDs = utilGetAllNodes(wayIDs, context.graph())
|
||||
.map(function(node) { return node.id; });
|
||||
if (wayNodeIDs.length <= 2) return false;
|
||||
|
||||
// If range of 2 selected nodes is supplied, ensure nodes lie on the selected path
|
||||
if (nodeIDs.length === 2 && (
|
||||
wayNodeIDs.indexOf(nodeIDs[0]) === -1 || wayNodeIDs.indexOf(nodeIDs[1]) === -1
|
||||
)) return false;
|
||||
|
||||
geometry = 'line';
|
||||
return actionStraightenWay(selectedIDs, context.projection);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function operation() {
|
||||
if (!action) return;
|
||||
context.perform(action, operation.annotation());
|
||||
}
|
||||
|
||||
|
||||
operation.available = function() {
|
||||
var nodeIDs = nodes.map(function(node) { return node.id; });
|
||||
var startNodeIDs = [];
|
||||
var endNodeIDs = [];
|
||||
var selectedNodeIDs = [];
|
||||
|
||||
for (var i = 0; i < selectedIDs.length; i++) {
|
||||
var entity = context.entity(selectedIDs[i]);
|
||||
if (entity.type === 'node') {
|
||||
selectedNodeIDs.push(entity.id);
|
||||
continue;
|
||||
} else if (entity.type !== 'way' || entity.isClosed()) {
|
||||
return false; // exit early, can't straighten these
|
||||
}
|
||||
|
||||
startNodeIDs.push(entity.first());
|
||||
endNodeIDs.push(entity.last());
|
||||
}
|
||||
|
||||
// Remove duplicate end/startNodeIDs (duplicate nodes cannot be at the line end)
|
||||
startNodeIDs = startNodeIDs.filter(function(n) {
|
||||
return startNodeIDs.indexOf(n) === startNodeIDs.lastIndexOf(n);
|
||||
});
|
||||
endNodeIDs = endNodeIDs.filter(function(n) {
|
||||
return endNodeIDs.indexOf(n) === endNodeIDs.lastIndexOf(n);
|
||||
});
|
||||
|
||||
// Return false if line is only 2 nodes long
|
||||
if (nodeIDs.length <= 2) return false;
|
||||
|
||||
// Return false unless exactly 0 or 2 specific start/end nodes are selected
|
||||
if (!(selectedNodeIDs.length === 0 || selectedNodeIDs.length === 2)) return false;
|
||||
|
||||
// Ensure all ways are connected (i.e. only 2 unique endpoints/startpoints)
|
||||
if (utilArrayDifference(startNodeIDs, endNodeIDs).length +
|
||||
utilArrayDifference(endNodeIDs, startNodeIDs).length !== 2) return false;
|
||||
|
||||
// Ensure both start/end selected nodes lie on the selected path
|
||||
if (selectedNodeIDs.length === 2 && (
|
||||
nodeIDs.indexOf(selectedNodeIDs[0]) === -1 || nodeIDs.indexOf(selectedNodeIDs[1]) === -1
|
||||
)) return false;
|
||||
|
||||
return true;
|
||||
return Boolean(action);
|
||||
};
|
||||
|
||||
|
||||
@@ -96,12 +112,12 @@ export function operationStraighten(selectedIDs, context) {
|
||||
var disable = operation.disabled();
|
||||
return disable ?
|
||||
t('operations.straighten.' + disable) :
|
||||
t('operations.straighten.description');
|
||||
t('operations.straighten.description.' + geometry);
|
||||
};
|
||||
|
||||
|
||||
operation.annotation = function() {
|
||||
return t('operations.straighten.annotation');
|
||||
return t('operations.straighten.annotation.' + geometry);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user