modules/actions/circularize.js (202 lines of code) (raw):
import { median as d3_median } from 'd3-array';
import {
polygonArea as d3_polygonArea,
polygonHull as d3_polygonHull,
polygonCentroid as d3_polygonCentroid
} from 'd3-polygon';
import { vecInterp, vecLength, vecLengthSquare } from '@id-sdk/math';
import { utilArrayUniq } from '@id-sdk/util';
import { osmNode } from '../osm/node';
export function actionCircularize(wayId, projection, maxAngle) {
maxAngle = (maxAngle || 20) * Math.PI / 180;
var action = function(graph, t) {
if (t === null || !isFinite(t)) t = 1;
t = Math.min(Math.max(+t, 0), 1);
var way = graph.entity(wayId);
var origNodes = {};
graph.childNodes(way).forEach(function(node) {
if (!origNodes[node.id]) origNodes[node.id] = node;
});
if (!way.isConvex(graph)) {
graph = action.makeConvex(graph);
}
var nodes = utilArrayUniq(graph.childNodes(way));
var keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; });
var points = nodes.map(function(n) { return projection(n.loc); });
var keyPoints = keyNodes.map(function(n) { return projection(n.loc); });
var centroid = (points.length === 2) ? vecInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points);
var radius = d3_median(points, function(p) { return vecLength(centroid, p); });
var sign = d3_polygonArea(points) > 0 ? 1 : -1;
var ids, i, j, k;
// we need at least two key nodes for the algorithm to work
if (!keyNodes.length) {
keyNodes = [nodes[0]];
keyPoints = [points[0]];
}
if (keyNodes.length === 1) {
var index = nodes.indexOf(keyNodes[0]);
var oppositeIndex = Math.floor((index + nodes.length / 2) % nodes.length);
keyNodes.push(nodes[oppositeIndex]);
keyPoints.push(points[oppositeIndex]);
}
// key points and nodes are those connected to the ways,
// they are projected onto the circle, in between nodes are moved
// to constant intervals between key nodes, extra in between nodes are
// added if necessary.
for (i = 0; i < keyPoints.length; i++) {
var nextKeyNodeIndex = (i + 1) % keyNodes.length;
var startNode = keyNodes[i];
var endNode = keyNodes[nextKeyNodeIndex];
var startNodeIndex = nodes.indexOf(startNode);
var endNodeIndex = nodes.indexOf(endNode);
var numberNewPoints = -1;
var indexRange = endNodeIndex - startNodeIndex;
var nearNodes = {};
var inBetweenNodes = [];
var startAngle, endAngle, totalAngle, eachAngle;
var angle, loc, node, origNode;
if (indexRange < 0) {
indexRange += nodes.length;
}
// position this key node
var distance = vecLength(centroid, keyPoints[i]) || 1e-4;
keyPoints[i] = [
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius
];
loc = projection.invert(keyPoints[i]);
node = keyNodes[i];
origNode = origNodes[node.id];
node = node.move(vecInterp(origNode.loc, loc, t));
graph = graph.replace(node);
// figure out the between delta angle we want to match to
startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]);
endAngle = Math.atan2(keyPoints[nextKeyNodeIndex][1] - centroid[1], keyPoints[nextKeyNodeIndex][0] - centroid[0]);
totalAngle = endAngle - startAngle;
// detects looping around -pi/pi
if (totalAngle * sign > 0) {
totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));
}
do {
numberNewPoints++;
eachAngle = totalAngle / (indexRange + numberNewPoints);
} while (Math.abs(eachAngle) > maxAngle);
// move existing nodes
for (j = 1; j < indexRange; j++) {
angle = startAngle + j * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle) * radius,
centroid[1] + Math.sin(angle) * radius
]);
node = nodes[(j + startNodeIndex) % nodes.length];
origNode = origNodes[node.id];
nearNodes[node.id] = angle;
node = node.move(vecInterp(origNode.loc, loc, t));
graph = graph.replace(node);
}
// add new in between nodes if necessary
for (j = 0; j < numberNewPoints; j++) {
angle = startAngle + (indexRange + j) * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle) * radius,
centroid[1] + Math.sin(angle) * radius
]);
// choose a nearnode to use as the original
var min = Infinity;
for (var nodeId in nearNodes) {
var nearAngle = nearNodes[nodeId];
var dist = Math.abs(nearAngle - angle);
if (dist < min) {
min = dist;
origNode = origNodes[nodeId];
}
}
node = osmNode({ loc: vecInterp(origNode.loc, loc, t) });
graph = graph.replace(node);
nodes.splice(endNodeIndex + j, 0, node);
inBetweenNodes.push(node.id);
}
// Check for other ways that share these keyNodes..
// If keyNodes are adjacent in both ways,
// we can add inBetweenNodes to that shared way too..
if (indexRange === 1 && inBetweenNodes.length) {
var startIndex1 = way.nodes.lastIndexOf(startNode.id);
var endIndex1 = way.nodes.lastIndexOf(endNode.id);
var wayDirection1 = (endIndex1 - startIndex1);
if (wayDirection1 < -1) { wayDirection1 = 1; }
var parentWays = graph.parentWays(keyNodes[i]);
for (j = 0; j < parentWays.length; j++) {
var sharedWay = parentWays[j];
if (sharedWay === way) continue;
if (sharedWay.areAdjacent(startNode.id, endNode.id)) {
var startIndex2 = sharedWay.nodes.lastIndexOf(startNode.id);
var endIndex2 = sharedWay.nodes.lastIndexOf(endNode.id);
var wayDirection2 = (endIndex2 - startIndex2);
var insertAt = endIndex2;
if (wayDirection2 < -1) { wayDirection2 = 1; }
if (wayDirection1 !== wayDirection2) {
inBetweenNodes.reverse();
insertAt = startIndex2;
}
for (k = 0; k < inBetweenNodes.length; k++) {
sharedWay = sharedWay.addNode(inBetweenNodes[k], insertAt + k);
}
graph = graph.replace(sharedWay);
}
}
}
}
// update the way to have all the new nodes
ids = nodes.map(function(n) { return n.id; });
ids.push(ids[0]);
way = way.update({nodes: ids});
graph = graph.replace(way);
return graph;
};
action.makeConvex = function(graph) {
var way = graph.entity(wayId);
var nodes = utilArrayUniq(graph.childNodes(way));
var points = nodes.map(function(n) { return projection(n.loc); });
var sign = d3_polygonArea(points) > 0 ? 1 : -1;
var hull = d3_polygonHull(points);
var i, j;
// D3 convex hulls go counterclockwise..
if (sign === -1) {
nodes.reverse();
points.reverse();
}
for (i = 0; i < hull.length - 1; i++) {
var startIndex = points.indexOf(hull[i]);
var endIndex = points.indexOf(hull[i+1]);
var indexRange = (endIndex - startIndex);
if (indexRange < 0) {
indexRange += nodes.length;
}
// move interior nodes to the surface of the convex hull..
for (j = 1; j < indexRange; j++) {
var point = vecInterp(hull[i], hull[i+1], j / indexRange);
var node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
graph = graph.replace(node);
}
}
return graph;
};
action.disabled = function(graph) {
if (!graph.entity(wayId).isClosed()) {
return 'not_closed';
}
//disable when already circular
var way = graph.entity(wayId);
var nodes = utilArrayUniq(graph.childNodes(way));
var points = nodes.map(function(n) { return projection(n.loc); });
var hull = d3_polygonHull(points);
var epsilonAngle = Math.PI / 180;
if (hull.length !== points.length || hull.length < 3){
return false;
}
var centroid = d3_polygonCentroid(points);
var radius = vecLengthSquare(centroid, points[0]);
var i, actualPoint;
// compare distances between centroid and points
for (i = 0; i < hull.length; i++){
actualPoint = hull[i];
var actualDist = vecLengthSquare(actualPoint, centroid);
var diff = Math.abs(actualDist - radius);
//compare distances with epsilon-error (5%)
if (diff > 0.05*radius) {
return false;
}
}
//check if central angles are smaller than maxAngle
for (i = 0; i < hull.length; i++){
actualPoint = hull[i];
var nextPoint = hull[(i+1)%hull.length];
var startAngle = Math.atan2(actualPoint[1] - centroid[1], actualPoint[0] - centroid[0]);
var endAngle = Math.atan2(nextPoint[1] - centroid[1], nextPoint[0] - centroid[0]);
var angle = endAngle - startAngle;
if (angle < 0) {
angle = -angle;
}
if (angle > Math.PI){
angle = (2*Math.PI - angle);
}
if (angle > maxAngle + epsilonAngle) {
return false;
}
}
return 'already_circular';
};
action.transitionable = true;
return action;
}