mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-14 05:12:13 +02:00
Change orthogonalize to support degenerate shapes, non-closed ways
Also adds a handful of useful vector math functions to geo module
This commit is contained in:
@@ -80,6 +80,7 @@ en:
|
||||
annotation:
|
||||
line: Squared the corners of a line.
|
||||
area: Squared the corners of an area.
|
||||
square_enough: This can't be made more square than it already is.
|
||||
not_squarish: This can't be made square because it is not squarish.
|
||||
too_large: This can't be made square because not enough of it is currently visible.
|
||||
connected_to_hidden: This can't be made square because it is connected to a hidden feature.
|
||||
|
||||
Vendored
+1
@@ -105,6 +105,7 @@
|
||||
"line": "Squared the corners of a line.",
|
||||
"area": "Squared the corners of an area."
|
||||
},
|
||||
"square_enough": "This can't be made more square than it already is.",
|
||||
"not_squarish": "This can't be made square because it is not squarish.",
|
||||
"too_large": "This can't be made square because not enough of it is currently visible.",
|
||||
"connected_to_hidden": "This can't be made square because it is connected to a hidden feature."
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import _clone from 'lodash-es/clone';
|
||||
import _uniq from 'lodash-es/uniq';
|
||||
import _cloneDeep from 'lodash-es/cloneDeep';
|
||||
|
||||
import { actionDeleteNode } from './delete_node';
|
||||
import {
|
||||
geoVecAdd,
|
||||
geoVecEqual,
|
||||
geoVecInterp,
|
||||
geoVecLength,
|
||||
geoVecNormalize,
|
||||
geoVecNormalizedDot,
|
||||
geoVecProject,
|
||||
geoVecScale,
|
||||
geoVecSubtract
|
||||
} from '../geo';
|
||||
@@ -17,7 +19,10 @@ import {
|
||||
* Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as
|
||||
*/
|
||||
export function actionOrthogonalize(wayID, projection) {
|
||||
var threshold = 12; // degrees within right or straight to alter
|
||||
var epsilon = 1e-4;
|
||||
var threshold = 13; // degrees within right or straight to alter
|
||||
|
||||
// We test normalized dot products so we can compare as cos(angle)
|
||||
var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);
|
||||
var upperThreshold = Math.cos(threshold * Math.PI / 180);
|
||||
|
||||
@@ -27,16 +32,32 @@ export function actionOrthogonalize(wayID, projection) {
|
||||
t = Math.min(Math.max(+t, 0), 1);
|
||||
|
||||
var way = graph.entity(wayID);
|
||||
var nodes = graph.childNodes(way);
|
||||
var points = _uniq(nodes).map(function(n) { return projection(n.loc); });
|
||||
var corner = {i: 0, dotp: 1};
|
||||
var epsilon = 1e-4;
|
||||
var node, loc, score, motions, i, j;
|
||||
way = way.removeNode(''); // sanity check - remove any consecutive duplicates
|
||||
graph = graph.replace(way);
|
||||
|
||||
var isClosed = way.isClosed();
|
||||
var nodes = _clone(graph.childNodes(way));
|
||||
if (isClosed) nodes.pop();
|
||||
|
||||
// note: all geometry functions here use the unclosed node/point/coord list
|
||||
|
||||
var nodeCount = {};
|
||||
var points = [];
|
||||
var corner = { i: 0, dotp: 1 };
|
||||
var node, point, loc, score, motions, i, j;
|
||||
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
nodeCount[node.id] = (nodeCount[node.id] || 0) + 1;
|
||||
points.push({ id: node.id, coord: projection(node.loc) });
|
||||
}
|
||||
|
||||
|
||||
if (points.length === 3) { // move only one vertex for right triangle
|
||||
for (i = 0; i < 1000; i++) {
|
||||
motions = points.map(calcMotion);
|
||||
points[corner.i] = geoVecAdd(points[corner.i], motions[corner.i]);
|
||||
|
||||
points[corner.i].coord = geoVecAdd(points[corner.i].coord, motions[corner.i]);
|
||||
score = corner.dotp;
|
||||
if (score < epsilon) {
|
||||
break;
|
||||
@@ -44,22 +65,45 @@ export function actionOrthogonalize(wayID, projection) {
|
||||
}
|
||||
|
||||
node = graph.entity(nodes[corner.i].id);
|
||||
loc = projection.invert(points[corner.i]);
|
||||
loc = projection.invert(points[corner.i].coord);
|
||||
graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));
|
||||
|
||||
} else {
|
||||
var best;
|
||||
var originalPoints = _clone(points);
|
||||
var straights = [];
|
||||
var simplified = [];
|
||||
|
||||
// Remove points from nearly straight sections..
|
||||
// This produces a simplified shape to orthogonalize
|
||||
for (i = 0; i < points.length; i++) {
|
||||
point = points[i];
|
||||
var dotp = 0;
|
||||
if (isClosed || (i > 0 && i < points.length - 1)) {
|
||||
var a = points[(i - 1 + points.length) % points.length];
|
||||
var b = points[(i + 1) % points.length];
|
||||
dotp = Math.abs(normalizedDotProduct(a.coord, b.coord, point.coord));
|
||||
}
|
||||
|
||||
if (dotp > upperThreshold) {
|
||||
straights.push(point);
|
||||
} else {
|
||||
simplified.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
// Orthogonalize the simplified shape
|
||||
var bestPoints = _cloneDeep(simplified);
|
||||
var originalPoints = _cloneDeep(simplified);
|
||||
score = Infinity;
|
||||
|
||||
for (i = 0; i < 1000; i++) {
|
||||
motions = points.map(calcMotion);
|
||||
motions = simplified.map(calcMotion);
|
||||
|
||||
for (j = 0; j < motions.length; j++) {
|
||||
points[j] = geoVecAdd(points[j],motions[j]);
|
||||
simplified[j].coord = geoVecAdd(simplified[j].coord, motions[j]);
|
||||
}
|
||||
var newScore = squareness(points);
|
||||
var newScore = calcScore(simplified, isClosed);
|
||||
if (newScore < score) {
|
||||
best = _clone(points);
|
||||
bestPoints = _cloneDeep(simplified);
|
||||
score = newScore;
|
||||
}
|
||||
if (score < epsilon) {
|
||||
@@ -67,30 +111,41 @@ export function actionOrthogonalize(wayID, projection) {
|
||||
}
|
||||
}
|
||||
|
||||
points = best;
|
||||
var bestCoords = bestPoints.map(function(p) { return p.coord; });
|
||||
if (isClosed) bestCoords.push(bestCoords[0]);
|
||||
|
||||
for (i = 0; i < points.length; i++) {
|
||||
// only move the points that actually moved
|
||||
if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) {
|
||||
loc = projection.invert(points[i]);
|
||||
node = graph.entity(nodes[i].id);
|
||||
// move the nodes that should move
|
||||
for (i = 0; i < bestPoints.length; i++) {
|
||||
point = bestPoints[i];
|
||||
if (!geoVecEqual(originalPoints[i].coord, point.coord)) {
|
||||
node = graph.entity(point.id);
|
||||
loc = projection.invert(point.coord);
|
||||
graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));
|
||||
}
|
||||
}
|
||||
|
||||
// remove empty nodes on straight sections
|
||||
for (i = 0; t === 1 && i < points.length; i++) {
|
||||
node = graph.entity(nodes[i].id);
|
||||
// move the nodes along straight segments
|
||||
for (i = 0; i < straights.length; i++) {
|
||||
point = straights[i];
|
||||
if (nodeCount[point.id] > 1) continue; // skip self-intersections
|
||||
|
||||
if (graph.parentWays(node).length > 1 ||
|
||||
graph.parentRelations(node).length ||
|
||||
node.hasInterestingTags()) {
|
||||
continue;
|
||||
}
|
||||
node = graph.entity(point.id);
|
||||
|
||||
var dotp = normalizedDotProduct(i, points);
|
||||
if (dotp < -1 + epsilon) {
|
||||
if (t === 1 &&
|
||||
graph.parentWays(node).length === 1 &&
|
||||
graph.parentRelations(node).length === 0 &&
|
||||
!node.hasInterestingTags()
|
||||
) {
|
||||
// remove uninteresting points..
|
||||
graph = actionDeleteNode(node.id)(graph);
|
||||
|
||||
} else {
|
||||
// move interesting points to the nearest edge..
|
||||
var choice = geoVecProject(point.coord, bestCoords);
|
||||
if (choice) {
|
||||
loc = projection.invert(choice.target);
|
||||
graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,74 +153,121 @@ export function actionOrthogonalize(wayID, projection) {
|
||||
return graph;
|
||||
|
||||
|
||||
function calcMotion(b, i, array) {
|
||||
var a = array[(i - 1 + array.length) % array.length];
|
||||
var c = array[(i + 1) % array.length];
|
||||
var p = geoVecSubtract(a, b);
|
||||
var q = geoVecSubtract(c, b);
|
||||
function calcMotion(point, i, array) {
|
||||
// don't try to move the endpoints of a non-closed way.
|
||||
if (!isClosed && (i === 0 || i === array.length - 1)) return [0, 0];
|
||||
// don't try to move a node that appears more than once (self intersection)
|
||||
if (nodeCount[array[i].id] > 1) return [0, 0];
|
||||
|
||||
var a = array[(i - 1 + array.length) % array.length].coord;
|
||||
var origin = point.coord;
|
||||
var b = array[(i + 1) % array.length].coord;
|
||||
var p = geoVecSubtract(a, origin);
|
||||
var q = geoVecSubtract(b, origin);
|
||||
|
||||
var scale = 2 * Math.min(geoVecLength(p), geoVecLength(q));
|
||||
p = geoVecNormalize(p);
|
||||
q = geoVecNormalize(q);
|
||||
|
||||
var dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]);
|
||||
var dotp = (p[0] * q[0] + p[1] * q[1]);
|
||||
var val = Math.abs(dotp);
|
||||
|
||||
// nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
|
||||
if (array.length > 3) {
|
||||
if (dotp < -0.707106781186547) {
|
||||
dotp += 1.0;
|
||||
}
|
||||
} else if (dotp && Math.abs(dotp) < corner.dotp) {
|
||||
if (val < lowerThreshold) { // nearly orthogonal
|
||||
corner.i = i;
|
||||
corner.dotp = Math.abs(dotp);
|
||||
corner.dotp = val;
|
||||
var vec = geoVecNormalize(geoVecAdd(p, q));
|
||||
return geoVecScale(vec, 0.1 * dotp * scale);
|
||||
}
|
||||
|
||||
var vec = geoVecNormalize(geoVecAdd(p, q));
|
||||
return geoVecScale(vec, 0.1 * dotp * scale);
|
||||
return [0, 0]; // do nothing
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function squareness(points) {
|
||||
return points.reduce(function(sum, val, i, array) {
|
||||
var dotp = normalizedDotProduct(i, array);
|
||||
dotp = filterDotProduct(dotp);
|
||||
return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
function normalizedDotProduct(i, points) {
|
||||
var a = points[(i - 1 + points.length) % points.length];
|
||||
var origin = points[i];
|
||||
var b = points[(i + 1) % points.length];
|
||||
function normalizedDotProduct(a, b, origin) {
|
||||
if (geoVecEqual(origin, a) || geoVecEqual(origin, b)) {
|
||||
return 1; // coincident points, treat as straight and try to remove
|
||||
}
|
||||
return geoVecNormalizedDot(a, b, origin);
|
||||
}
|
||||
|
||||
|
||||
function filterDotProduct(dotp) {
|
||||
if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) {
|
||||
return dotp;
|
||||
var val = Math.abs(dotp);
|
||||
if (val < epsilon) {
|
||||
return 0; // already orthogonal
|
||||
} else if (val < lowerThreshold || val > upperThreshold) {
|
||||
return dotp; // can be adjusted
|
||||
} else {
|
||||
return null; // ignore vertex
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
function calcScore(points, isClosed) {
|
||||
var score = 0;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? points.length : points.length - 1;
|
||||
var coords = points.map(function(p) { return p.coord; });
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var dotp = filterDotProduct(normalizedDotProduct(a, b, origin));
|
||||
if (dotp === null) continue; // ignore vertex
|
||||
score = score + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
// similar to calcScore, but returns quickly if there is something to do
|
||||
function canOrthogonalize(coords, isClosed) {
|
||||
var score = null;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? coords.length : coords.length - 1;
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var val = filterDotProduct(normalizedDotProduct(a, b, origin));
|
||||
if (val === null) continue; // ignore vertex
|
||||
if (val > 0) return 1; // something to do
|
||||
score = 0; // already square
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
action.disabled = function(graph) {
|
||||
var way = graph.entity(wayID);
|
||||
var nodes = graph.childNodes(way);
|
||||
var points = _uniq(nodes).map(function(n) { return projection(n.loc); });
|
||||
way = way.removeNode(''); // sanity check - remove any consecutive duplicates
|
||||
graph = graph.replace(way);
|
||||
|
||||
if (squareness(points)) {
|
||||
var isClosed = way.isClosed();
|
||||
var nodes = _clone(graph.childNodes(way));
|
||||
if (isClosed) nodes.pop();
|
||||
|
||||
var coords = nodes.map(function(n) { return projection(n.loc); });
|
||||
var score = canOrthogonalize(coords, isClosed);
|
||||
|
||||
if (score === null) {
|
||||
return 'not_squarish';
|
||||
} else if (score === 0) {
|
||||
return 'square_enough';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'not_squarish';
|
||||
};
|
||||
|
||||
|
||||
action.transitionable = true;
|
||||
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -38,5 +38,6 @@ export { geoVecInterp } from './vector.js';
|
||||
export { geoVecLength } from './vector.js';
|
||||
export { geoVecNormalize } from './vector.js';
|
||||
export { geoVecNormalizedDot } from './vector.js';
|
||||
export { geoVecProject } from './vector.js';
|
||||
export { geoVecSubtract } from './vector.js';
|
||||
export { geoVecScale } from './vector.js';
|
||||
|
||||
@@ -84,3 +84,40 @@ export function geoVecCross(a, b, origin) {
|
||||
return (p[0]) * (q[1]) - (p[1]) * (q[0]);
|
||||
}
|
||||
|
||||
|
||||
// find closest orthogonal projection of point onto points array
|
||||
export function geoVecProject(a, points) {
|
||||
var min = Infinity;
|
||||
var idx;
|
||||
var target;
|
||||
|
||||
for (var i = 0; i < points.length - 1; i++) {
|
||||
var o = points[i];
|
||||
var s = geoVecSubtract(points[i + 1], o);
|
||||
var v = geoVecSubtract(a, 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 dist = geoVecLength(p, a);
|
||||
if (dist < min) {
|
||||
min = dist;
|
||||
idx = i + 1;
|
||||
target = p;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx !== undefined) {
|
||||
return { index: idx, distance: min, target: target };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,6 @@ _extend(osmWay.prototype, {
|
||||
|
||||
// returns an object with the tag that implies this is an area, if any
|
||||
tagSuggestingArea: function() {
|
||||
|
||||
if (this.tags.area === 'yes') return { area: 'yes' };
|
||||
if (this.tags.area === 'no') return null;
|
||||
|
||||
@@ -230,7 +229,6 @@ _extend(osmWay.prototype, {
|
||||
},
|
||||
|
||||
isArea: function() {
|
||||
|
||||
if (this.tags.area === 'yes')
|
||||
return true;
|
||||
if (!this.isClosed() || this.tags.area === 'no')
|
||||
|
||||
Reference in New Issue
Block a user