This repository has been archived by the owner on Jul 27, 2021. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
82ac169
commit 6437706
Showing
2 changed files
with
327 additions
and
413 deletions.
There are no files selected for viewing
326 changes: 326 additions & 0 deletions
326
src/org/openstreetmap/josm/plugins/curves/CircleArcMaker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,326 @@ | ||
package org.openstreetmap.josm.plugins.curves; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.HashSet; | ||
import java.util.Iterator; | ||
import java.util.LinkedList; | ||
import java.util.List; | ||
import java.util.Set; | ||
|
||
import org.openstreetmap.josm.command.AddCommand; | ||
import org.openstreetmap.josm.command.ChangeCommand; | ||
import org.openstreetmap.josm.command.Command; | ||
import org.openstreetmap.josm.data.coor.EastNorth; | ||
import org.openstreetmap.josm.data.osm.Node; | ||
import org.openstreetmap.josm.data.osm.OsmPrimitive; | ||
import org.openstreetmap.josm.data.osm.Way; | ||
|
||
public class CircleArcMaker { | ||
public static Collection<Command> doCircleArc(List<Node> selectedNodes, List<Way> selectedWays) { | ||
Collection<Command> cmds = new LinkedList<Command>(); | ||
|
||
//// Decides which nodes to use as anchors based on selection | ||
/* | ||
* Rules goes like this: | ||
* If there are selected ways, at least one of these are used as target ways for the arc. | ||
* Selected ways override selected nodes. If nodes not part of the ways are selected they're ignored. | ||
* | ||
* When existing ways are reused for the arc, all ways overlapping these are transformed too. | ||
* | ||
* 1. Exactly 3 nodes selected: | ||
* Use these nodes. | ||
* - No way selected: create a new way. | ||
* 2. Exactly 1 node selected, node part of exactly 1 way with 3 or more nodes: | ||
* Node selected used as first node, consequent nodes in the way's direction used as the rest. | ||
* - Reversed if not enough nodes in forward direction | ||
* - Selected node used as middle node its the middle node in a 3 node way | ||
* - Parent way used | ||
*/ | ||
|
||
//// Anchor nodes | ||
Node n1 = null, n2 = null, n3 = null; | ||
|
||
if (false) { | ||
int nodeCount = selectedNodes.size(); | ||
int wayCount = selectedWays.size(); | ||
|
||
// TODO: filter garbage nodes based on selected ways | ||
|
||
// Never interested in more than 3 nodes. Nodes prioritized by reverse selection order, but keep their order. | ||
// TODO: replace by helper function (eg. getPostFixList(int count)) | ||
Node[] nodesOfInterest = new Node[3]; | ||
int nodesOfInterestCount = Math.min(nodeCount, 3); | ||
for (int i = nodesOfInterestCount - 1; i >= 0; i--) { | ||
nodesOfInterest[i] = selectedNodes.get(nodeCount - 1 - i); | ||
} | ||
} | ||
|
||
Set<Way> targetWays = new HashSet<Way>(); | ||
|
||
boolean nodesHasBeenChoosen = false; | ||
if (selectedNodes.size() == 3) { | ||
Iterator<Node> nodeIter = selectedNodes.iterator(); | ||
n1 = nodeIter.next(); | ||
n2 = nodeIter.next(); | ||
n3 = nodeIter.next(); | ||
nodesHasBeenChoosen = true; | ||
if (selectedWays.isEmpty()) { // Create a brand new way | ||
Way newWay = new Way(); | ||
targetWays.add(newWay); | ||
cmds.add(new AddCommand(newWay)); | ||
newWay.addNode(n1); | ||
newWay.addNode(n2); | ||
newWay.addNode(n3); | ||
} | ||
} | ||
if (selectedWays.isEmpty() == false) { | ||
// TODO: use only two nodes inferring the orientation from the parent way. | ||
|
||
if (nodesHasBeenChoosen == false) { | ||
// Use the three last nodes in the way as anchors. This is intended to be used with the | ||
// built in draw mode | ||
Way w = selectedWays.iterator().next(); //TODO: select last selected way instead | ||
int nodeCount = w.getNodesCount(); | ||
if (nodeCount < 3) | ||
return null; | ||
n3 = w.getNode(nodeCount - 1); | ||
n2 = w.getNode(nodeCount - 2); | ||
n1 = w.getNode(nodeCount - 3); | ||
nodesHasBeenChoosen = true; | ||
} | ||
targetWays.addAll(OsmPrimitive.getFilteredList(n1.getReferrers(), Way.class)); | ||
targetWays.addAll(OsmPrimitive.getFilteredList(n2.getReferrers(), Way.class)); | ||
targetWays.addAll(OsmPrimitive.getFilteredList(n3.getReferrers(), Way.class)); | ||
// for(Way w : selectedWays) { | ||
// targetWays.add(w); | ||
// } | ||
|
||
} | ||
if (nodesHasBeenChoosen == false) { | ||
return null; | ||
} | ||
|
||
EastNorth p1 = n1.getEastNorth(); | ||
EastNorth p2 = n2.getEastNorth(); | ||
EastNorth p3 = n3.getEastNorth(); | ||
// TODO: Check that the points are distinct | ||
|
||
// // Calculate the new points in the segment | ||
List<EastNorth> points = circleSeqmentPoints(p1, p2, p3, 15, true, null); | ||
|
||
//// Create the new arc nodes. Insert anchor nodes at correct positions. | ||
List<Node> arcNodes = new ArrayList<Node>(points.size()); | ||
arcNodes.add(n1); | ||
for (EastNorth p : slice(points, 1, -2)) { | ||
if (p == p2) { | ||
arcNodes.add(n2); | ||
} else { | ||
Node n = new Node(p); | ||
arcNodes.add(n); | ||
cmds.add(new AddCommand(n)); | ||
} | ||
} | ||
arcNodes.add(n3); | ||
|
||
//// "Fuse" the arc with all target ways | ||
|
||
Node[] anchorNodes = { n1, n2, n3 }; | ||
for (Way originalTw : targetWays) { | ||
Way tw = new Way(originalTw); | ||
boolean didChangeTw = false; | ||
/// Do one segment at the time (so ways only sharing one segment is fused too) | ||
for (int a = 0; a < 2; a++) { | ||
int anchorBi = arcNodes.indexOf(anchorNodes[a]); // TODO: optimize away | ||
int anchorEi = arcNodes.indexOf(anchorNodes[a + 1]); | ||
/// Find the anchor node indices in current target way | ||
int bi = -1, ei = -1; | ||
int i = -1; | ||
// Caution: nodes might appear multiple times. For now only handle simple closed ways | ||
for (Node n : tw.getNodes()) { | ||
i++; | ||
// We look for the first anchor node. The next should be directly to the left or right. | ||
// Exception when the way is closed | ||
if (n == anchorNodes[a]) { | ||
bi = i; | ||
Node otherAnchor = anchorNodes[a + 1]; | ||
if (i > 0 && tw.getNode(i - 1) == otherAnchor) { | ||
ei = i - 1; | ||
} else if (i < (tw.getNodesCount() - 1) && tw.getNode(i + 1) == otherAnchor) { | ||
ei = i + 1; | ||
} else { | ||
continue; // this can happen with closed ways. Continue searching for the correct index | ||
} | ||
break; | ||
} | ||
} | ||
if (bi == -1 || ei == -1) { | ||
continue; // this segment is not part of the target way | ||
} | ||
didChangeTw = true; | ||
|
||
/// Insert the nodes of this segment | ||
// Direction of target way relative to the arc node order | ||
int twDirection = ei > bi ? 1 : 0; | ||
int anchorI = anchorBi + 1; // don't insert the anchor nodes again | ||
int twI = bi + (twDirection == 1 ? 1 : 0); // TODO: explain | ||
while (anchorI < anchorEi) { | ||
tw.addNode(twI, arcNodes.get(anchorI)); | ||
anchorI++; | ||
twI += twDirection; | ||
} | ||
} | ||
if (didChangeTw) | ||
cmds.add(new ChangeCommand(originalTw, tw)); | ||
} | ||
|
||
return cmds; | ||
} | ||
|
||
|
||
/** | ||
* Return a list of coordinates lying an the circle segment determined by n1, n2 and n3. | ||
* The order of the list and which of the 3 possible segments are given by the order of n1, n2, n3 | ||
* | ||
* @param includeAnchors include the anchorpoints in the list. The original objects will be used, not copies | ||
*/ | ||
private static List<EastNorth> circleSeqmentPoints(EastNorth p1, EastNorth p2, EastNorth p3, | ||
int resolution, boolean includeAnchors, int[] anchor2Index) { | ||
|
||
// triangle: three single nodes needed or a way with three nodes | ||
|
||
// let's get some shorter names | ||
double x1 = p1.east(); | ||
double y1 = p1.north(); | ||
double x2 = p2.east(); | ||
double y2 = p2.north(); | ||
double x3 = p3.east(); | ||
double y3 = p3.north(); | ||
|
||
// calculate the center (xc,yc) | ||
double s = 0.5 * ((x2 - x3) * (x1 - x3) - (y2 - y3) * (y3 - y1)); | ||
double sUnder = (x1 - x2) * (y3 - y1) - (y2 - y1) * (x1 - x3); | ||
|
||
assert (sUnder == 0); | ||
|
||
s /= sUnder; | ||
|
||
double xc = 0.5 * (x1 + x2) + s * (y2 - y1); | ||
double yc = 0.5 * (y1 + y2) + s * (x1 - x2); | ||
|
||
// calculate the radius (r) | ||
double r = Math.sqrt(Math.pow(xc - x1, 2) + Math.pow(yc - y1, 2)); | ||
|
||
// The angles of the anchor points relative to the center | ||
double realA1 = calcang(xc, yc, x1, y1); | ||
double realA2 = calcang(xc, yc, x2, y2); | ||
double realA3 = calcang(xc, yc, x3, y3); | ||
|
||
double startAngle = realA1; | ||
// Transform the angles to get a consistent starting point | ||
double a1 = 0; | ||
double a2 = normalizeAngle(realA2 - startAngle); | ||
double a3 = normalizeAngle(realA3 - startAngle); | ||
int direction = a3 > a2 ? 1 : -1; | ||
|
||
double radialLength = 0; | ||
if (direction == 1) { // counter clockwise | ||
radialLength = a3; | ||
} else { // clockwise | ||
radialLength = Math.PI * 2 - a3; | ||
// make the angles consistent with the direction. | ||
a2 = (Math.PI * 2 - a2); | ||
a3 = (Math.PI * 2 - a3); | ||
} | ||
int numberOfNodesInArc = (int) Math.ceil((radialLength / Math.PI) * 180 / resolution); | ||
List<EastNorth> points = new ArrayList<EastNorth>(numberOfNodesInArc); | ||
|
||
// Calculate the circle points in order | ||
double stepLength = radialLength / numberOfNodesInArc; | ||
// Determine closest index to p2 | ||
|
||
int indexJustBeforeP2 = (int) Math.floor(a2 / stepLength); | ||
int closestIndexToP2 = indexJustBeforeP2; | ||
if ((a2 - indexJustBeforeP2 * stepLength) > ((indexJustBeforeP2 + 1) * stepLength - a2)) { | ||
closestIndexToP2 = indexJustBeforeP2 + 1; | ||
} | ||
// can't merge with end node | ||
if (closestIndexToP2 == numberOfNodesInArc - 1) { | ||
closestIndexToP2--; | ||
} else if (closestIndexToP2 == 0) { | ||
closestIndexToP2++; | ||
} | ||
assert (closestIndexToP2 != 0); | ||
|
||
double a = direction * (stepLength); | ||
points.add(p1); | ||
if (indexJustBeforeP2 == 0 && includeAnchors) { | ||
points.add(p2); | ||
} | ||
// i is ahead of the real index by one, since we need to be ahead in the angle calculation | ||
for (int i = 2; i < numberOfNodesInArc + 1; i++) { | ||
double nextA = direction * (i * stepLength); | ||
double realAngle = a + startAngle; | ||
double x = xc + r * Math.cos(realAngle); | ||
double y = yc + r * Math.sin(realAngle); | ||
|
||
points.add(new EastNorth(x, y)); | ||
if (i - 1 == indexJustBeforeP2 && includeAnchors) { | ||
points.add(p2); | ||
} | ||
a = nextA; | ||
} | ||
points.add(p3); | ||
if (anchor2Index != null) { | ||
anchor2Index[0] = closestIndexToP2; | ||
} | ||
return points; | ||
} | ||
|
||
// gah... why can't java support "reverse indices"? | ||
private static <T> List<T> slice(List<T> list, int from, int to) { | ||
if (to < 0) | ||
to += list.size() + 1; | ||
return list.subList(from, to); | ||
} | ||
|
||
/** | ||
* Normalizes {@code a} so it is between 0 and 2 PI | ||
*/ | ||
private static double normalizeAngle(double angle) { | ||
double PI2 = Math.PI * 2; | ||
if (angle < 0) { | ||
angle = angle + (Math.floor(-angle / PI2) + 1) * PI2; | ||
} else if (angle >= PI2) { | ||
angle = angle - Math.floor(angle / PI2) * PI2; | ||
} | ||
return angle; | ||
} | ||
|
||
private static double calcang(double xc, double yc, double x, double y) { | ||
// calculate the angle from xc|yc to x|y | ||
if (xc == x && yc == y) | ||
return 0; // actually invalid, but we won't have this case in this context | ||
double yd = Math.abs(y - yc); | ||
if (yd == 0 && xc < x) | ||
return 0; | ||
if (yd == 0 && xc > x) | ||
return Math.PI; | ||
double xd = Math.abs(x - xc); | ||
double a = Math.atan2(xd, yd); | ||
if (y > yc) { | ||
a = Math.PI - a; | ||
} | ||
if (x < xc) { | ||
a = -a; | ||
} | ||
a = 1.5 * Math.PI + a; | ||
if (a < 0) { | ||
a += 2 * Math.PI; | ||
} | ||
if (a >= 2 * Math.PI) { | ||
a -= 2 * Math.PI; | ||
} | ||
return a; | ||
} | ||
} |
Oops, something went wrong.