Ticket #5179: osm-join-areas.patch
| File osm-join-areas.patch, 14.2 KB (added by , 16 years ago) |
|---|
-
src/org/openstreetmap/josm/actions/JoinAreasAction.java
6 6 import static org.openstreetmap.josm.tools.I18n.trn; 7 7 8 8 import java.awt.GridBagLayout; 9 import java.awt.Polygon;10 9 import java.awt.event.ActionEvent; 11 10 import java.awt.event.KeyEvent; 12 11 import java.awt.geom.Area; … … 185 184 if(!same) { 186 185 int i = 0; 187 186 if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 187 188 //join each area with itself, fixing self-crossings. 188 189 if(joinAreas(a, a)) { 189 190 ++i; 190 191 } … … 210 211 211 212 Collection<Way> allWays = splitWaysOnNodes(a, b, nodes); 212 213 213 // Find all nodes andinner ways save them to a list214 Collection< Node> allNodes = getNodesFromWays(allWays);215 Collection<Way> innerWays = findInnerWays(allWays, allNodes);214 // Find inner ways save them to a list 215 Collection<Way> outerWays = findOuterWays(allWays); 216 Collection<Way> innerWays = findInnerWays(allWays, outerWays); 216 217 217 218 // Join outer ways 218 Way outerWay = joinOuterWays( allWays, innerWays);219 Way outerWay = joinOuterWays(outerWays); 219 220 if (outerWay == null) 220 221 return true; 221 222 … … 538 539 return allNodes; 539 540 } 540 541 542 541 543 /** 542 * Finds all inner ways for a given list of Ways and Nodes from a multigon by constructing a polygon 543 * for each way, looking for inner nodes that are not part of this way. If a node is found, all ways 544 * containing this node are added to the list 544 * Gets all inner ways given all ways and outer ways. 545 * @param multigonWays 546 * @param outerWays 547 * @return list of inner ways. 548 */ 549 private Collection<Way> findInnerWays(Collection<Way> multigonWays,Collection<Way> outerWays) { 550 ArrayList<Way> innerWays = new ArrayList<Way>(); 551 for(Way way: multigonWays) { 552 if (!outerWays.contains(way)) { 553 innerWays.add(way); 554 } 555 } 556 557 return innerWays; 558 } 559 560 561 /** 562 * Finds all ways for a given list of Ways that form the outer hull. 563 * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 564 * Prerequisites - the ways must not intersect and have common end nodes where they meet. 545 565 * @param Collection<Way> A list of (splitted) ways that form a multigon 546 * @param Collection<Node> A list of nodes that belong to the multigon 547 * @return Collection<Way> A list of ways that are positioned inside the outer borders of the multigon 566 * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 548 567 */ 549 private Collection<Way> findInnerWays(Collection<Way> multigonWays, Collection<Node> multigonNodes) { 550 Collection<Way> innerWays = new ArrayList<Way>(); 551 for(Way w: multigonWays) { 552 Polygon poly = new Polygon(); 553 for(Node n: (w).getNodes()) { 554 poly.addPoint(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon())); 568 public static Collection<Way> findOuterWays(Collection<Way> multigonWays) { 569 570 //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 571 Way bestWay = null; 572 Node topNode = null; 573 int topIndex = 0; 574 double minLat = Double.POSITIVE_INFINITY; 575 576 for(Way way: multigonWays) { 577 for (int pos = 0; pos < way.getNodesCount(); pos ++){ 578 Node node = way.getNode(pos); 579 580 if (node.getCoor().lat() < minLat){ 581 minLat = node.getCoor().lat(); 582 bestWay = way; 583 topNode = node; 584 topIndex = pos; 585 } 555 586 } 587 } 556 588 557 for(Node n: multigonNodes) { 558 if(!(w).containsNode(n) && poly.contains(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon()))) { 559 getWaysByNode(innerWays, multigonWays, n); 589 //get two final nodes from best way to mark as starting point and orientation. 590 Node headNode = null; 591 Node prevNode = null; 592 593 if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 594 { 595 //node is in split point 596 headNode = topNode; 597 //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 598 prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 599 } 600 else 601 { 602 //node is inside way - pick the clockwise going end. 603 Node prev = bestWay.getNode(topIndex - 1); 604 Node next = bestWay.getNode(topIndex + 1); 605 606 if (angleIsClockwise(prev, topNode, next)){ 607 headNode = bestWay.lastNode(); 608 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 609 } 610 else 611 { 612 headNode = bestWay.firstNode(); 613 prevNode = bestWay.getNode(1); 614 } 615 } 616 617 ArrayList<Way> outerWays = new ArrayList<Way>(); 618 619 //iterate till full circle is reached 620 while (true){ 621 622 bestWay = null; 623 Node bestWayNextNode = null; 624 boolean bestWayReverse = false; 625 626 for (Way way: multigonWays) 627 { 628 boolean wayReverse; 629 Node nextNode; 630 631 if (way.firstNode().equals(headNode)){ 632 nextNode = way.getNode(1); 633 wayReverse = false; 560 634 } 635 else if (way.lastNode().equals(headNode)) 636 { 637 nextNode = way.getNode(way.getNodesCount() - 2); 638 wayReverse = true; 639 } 640 else 641 { 642 //this way not adjacent to headNode 643 continue; 644 } 645 646 if (nextNode.equals(prevNode)) 647 { 648 //this is the path we came from - ignore it. 649 } 650 else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 651 { 652 //the new way is better 653 bestWay = way; 654 bestWayReverse = wayReverse; 655 bestWayNextNode = nextNode; 656 } 561 657 } 658 659 if (bestWay == null) 660 //this should not happen. Internal error here. 661 return null; 662 else if (outerWays.contains(bestWay)){ 663 //full circle reached, terminate. 664 break; 665 } 666 else 667 { 668 //add to outer ways, repeat. 669 outerWays.add(bestWay); 670 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 671 prevNode = bestWayReverse ? bestWay.getNode(2) : bestWay.getNode(bestWay.getNodesCount() - 2); 672 } 562 673 } 563 674 564 return innerWays;675 return outerWays; 565 676 } 566 677 567 // Polygon only supports int coordinates, so convert them 568 private int latlonToXY(double val) { 569 return (int)Math.round(val*1000000); 678 /** 679 * Tests if given point is to the right side of path consisting of 3 points. 680 * @param lineP1 first point in path 681 * @param lineP2 second point in path 682 * @param lineP3 third point in path 683 * @param testPoint 684 * @return true if to the right side, false otherwise 685 */ 686 public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 687 { 688 boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 689 boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 690 boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 691 692 if (pathBendToRight) 693 return rightOfSeg1 && rightOfSeg2; 694 else 695 return !(!rightOfSeg1 && !rightOfSeg2); 570 696 } 571 697 572 698 /** 573 * Finds all ways that contain the given node. 574 * @param Collection<Way> A list to which matching ways will be added 575 * @param Collection<Way> A list of ways to check 576 * @param Node The node the ways should be checked against 699 * This method tests if secondNode is clockwise to first node. 700 * @param commonNode starting point for both vectors 701 * @param firstNode first vector end node 702 * @param secondNode second vector end node 703 * @return true if first vector is clockwise before second vector. 577 704 */ 578 private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 579 for(Way way : w) { 580 if(!(way).containsNode(n)) { 705 public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 706 { 707 double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 708 double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 709 double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 710 double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 711 712 return dla1 * dlo2 - dlo1 * dla2 > 0; 713 } 714 715 716 /** 717 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 718 * @param polygonNodes list of nodes from polygon path. 719 * @param point the point to test 720 * @return true if the point is inside polygon. 721 * FIXME: this should probably be moved to tools.. 722 */ 723 public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 724 { 725 if (polygonNodes.size() < 3) 726 return false; 727 728 boolean inside = false; 729 Node p1, p2; 730 731 //iterate each side of the polygon, start with the last segment 732 Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 733 734 for(Node newPoint: polygonNodes) 735 { 736 //skip duplicate points 737 if (newPoint.equals(oldPoint)) { 581 738 continue; 582 739 } 583 if(!innerWays.contains(way)) { 584 innerWays.add(way); // Will need this later for multigons 740 741 //order points so p1.lat <= p2.lat; 742 if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 743 { 744 p1 = oldPoint; 745 p2 = newPoint; 585 746 } 747 else 748 { 749 p1 = newPoint; 750 p2 = oldPoint; 751 } 752 753 //test if the line is crossed and if so invert the inside flag. 754 if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 755 && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 756 < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 757 { 758 inside = !inside; 759 } 760 761 oldPoint = newPoint; 586 762 } 763 764 return inside; 587 765 } 588 766 767 768 769 589 770 /** 590 * Joins the two outer ways and deletes all short ways that can't be part of a multipolygon anyway 591 * @param Collection<OsmPrimitive> The list of all ways that belong to that multigon 592 * @param Collection<Way> The list of inner ways that belong to that multigon 771 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 772 * @param Collection<Way> The list of outer ways that belong to that multigon. 593 773 * @return Way The newly created outer way 594 774 */ 595 private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) {775 private Way joinOuterWays(Collection<Way> outerWays) { 596 776 ArrayList<Way> join = new ArrayList<Way>(); 597 for(Way w: multigonWays) { 598 // Skip inner ways 599 if(innerWays.contains(w)) { 600 continue; 601 } 777 for(Way w: outerWays) { 602 778 603 779 if(w.getNodesCount() <= 2) { 604 780 cmds.add(new DeleteCommand(w)); … … 980 1156 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 981 1157 setEnabled(selection != null && !selection.isEmpty()); 982 1158 } 983 } 1159 1160 } 1161 No newline at end of file -
test/unit/actions/JoinAreasActionTest.java
1 // License: GPL. For details, see LICENSE file. 2 package actions; 3 4 import org.junit.Assert; 5 import org.junit.Test; 6 import org.openstreetmap.josm.actions.JoinAreasAction; 7 import org.openstreetmap.josm.data.coor.LatLon; 8 import org.openstreetmap.josm.data.osm.Node; 9 10 11 public class JoinAreasActionTest { 12 13 private Node makeNode(double lat, double lon) 14 { 15 Node node = new Node(new LatLon(lat, lon)); 16 return node; 17 } 18 19 @Test 20 public void testAngleIsClockwise() 21 { 22 Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(0,0), makeNode(1,1), makeNode(0,1))); 23 Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(0,0))); 24 Assert.assertTrue(!JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(1,0))); 25 } 26 27 @Test 28 public void testisToTheRightSideOfLine() 29 { 30 Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(0, 0.5))); 31 Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(1, 0))); 32 Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(0,0))); 33 Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(2, 0))); 34 } 35 36 }
