Ticket #5179: osm-join-areas-3.patch
File osm-join-areas-3.patch, 19.4 KB (added by , 14 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; … … 30 29 import javax.swing.JPanel; 31 30 32 31 import org.openstreetmap.josm.Main; 32 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 33 33 import org.openstreetmap.josm.command.AddCommand; 34 34 import org.openstreetmap.josm.command.ChangeCommand; 35 35 import org.openstreetmap.josm.command.Command; … … 185 185 if(!same) { 186 186 int i = 0; 187 187 if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 188 189 //join each area with itself, fixing self-crossings. 188 190 if(joinAreas(a, a)) { 189 191 ++i; 190 192 } … … 195 197 cmdsCount = i; 196 198 } 197 199 198 ArrayList< OsmPrimitive> nodes = addIntersections(a, b);200 ArrayList<Node> nodes = addIntersections(a, b); 199 201 if(nodes.size() == 0) return hadChanges; 200 202 commitCommands(marktr("Added node on all intersections")); 201 203 … … 208 210 // Don't warn now, because it will really look corrupted 209 211 boolean warnAboutRelations = relations.size() > 0; 210 212 211 Collection<Way> allWays = splitWaysOnNodes(a, b, nodes);213 ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes); 212 214 213 // Find all nodes andinner ways save them to a list214 Collection<Node> allNodes = getNodesFromWays(allWays);215 Collection<Way> innerWays = findInnerWays(allWays, allNodes);215 // Find inner ways save them to a list 216 ArrayList<Way> outerWays = findOuterWays(allWays); 217 ArrayList<Way> innerWays = findInnerWays(allWays, outerWays); 216 218 217 219 // Join outer ways 218 Way outerWay = joinOuterWays( allWays, innerWays);220 Way outerWay = joinOuterWays(outerWays); 219 221 if (outerWay == null) 220 222 return true; 221 223 … … 330 332 * @param Way Second way 331 333 * @return ArrayList<OsmPrimitive> List of new nodes 332 334 */ 333 private ArrayList< OsmPrimitive> addIntersections(Way a, Way b) {335 private ArrayList<Node> addIntersections(Way a, Way b) { 334 336 boolean same = a.equals(b); 335 337 int nodesSizeA = a.getNodesCount(); 336 338 int nodesSizeB = b.getNodesCount(); 337 339 338 // We use OsmPrimitive here instead of Node because we later need to split a way at these nodes. 339 // With OsmPrimitve we can simply add the way and don't have to loop over the nodes 340 ArrayList<OsmPrimitive> nodes = new ArrayList<OsmPrimitive>(); 340 ArrayList<Node> nodes = new ArrayList<Node>(); 341 341 ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>(); 342 342 ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>(); 343 343 … … 488 488 } 489 489 490 490 /** 491 * This is a hacky implementation to make use of the splitWayAction code and 492 * should be improved. SplitWayAction needs to expose its splitWay function though. 491 * This is a method splits ways into smaller parts, using the prepared nodes list as split points. 492 * Uses SplitWayAction.splitWay for the heavy lifting. 493 * @return list of split ways (or original ways if no splitting is done). 493 494 */ 494 private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) { 495 ArrayList<Way> ways = new ArrayList<Way>(); 495 private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) { 496 497 ArrayList<Way> result = new ArrayList<Way>(); 498 List<Way> ways = new ArrayList<Way>(); 496 499 ways.add(a); 497 if(!a.equals(b)) { 498 ways.add(b); 500 ways.add(b); 501 502 for (Way way: ways){ 503 List<List<Node>> chunks = buildNodeChunks(way, nodes); 504 SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 505 506 //execute the command, we need the results 507 Main.main.undoRedo.add(split.getCommand()); 508 cmdsCount ++; 509 510 result.add(split.getOriginalWay()); 511 result.addAll(split.getNewWays()); 499 512 } 500 513 501 List<OsmPrimitive> affected = new ArrayList<OsmPrimitive>(); 502 for (Way way : ways) { 503 nodes.add(way); 504 Main.main.getCurrentDataSet().setSelected(nodes); 505 nodes.remove(way); 506 new SplitWayAction().actionPerformed(null); 507 cmdsCount++; 508 affected.addAll(Main.main.getCurrentDataSet().getSelectedWays()); 509 } 510 return osmprim2way(affected); 514 return result; 511 515 } 512 516 517 513 518 /** 514 * Converts a list of OsmPrimitives to a list of Ways 515 * @param Collection<OsmPrimitive> The OsmPrimitives list that's needed as a list of Ways 516 * @return Collection<Way> The list as list of Ways 519 * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on. 520 * @param way the way to chunk 521 * @param splitNodes the places where to cut. 522 * @return list of node segments to produce. 517 523 */ 518 static private Collection<Way> osmprim2way(Collection<OsmPrimitive> ways) { 519 Collection<Way> result = new ArrayList<Way>(); 520 for(OsmPrimitive w: ways) { 521 if(w instanceof Way) { 522 result.add((Way) w); 524 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) 525 { 526 List<List<Node>> result = new ArrayList<List<Node>>(); 527 List<Node> curList = new ArrayList<Node>(); 528 529 for(Node node: way.getNodes()){ 530 curList.add(node); 531 if (curList.size() > 1 && splitNodes.contains(node)){ 532 result.add(curList); 533 curList = new ArrayList<Node>(); 534 curList.add(node); 523 535 } 524 536 } 537 538 if (curList.size() > 1) 539 { 540 result.add(curList); 541 } 542 525 543 return result; 526 544 } 527 545 546 528 547 /** 529 548 * Returns all nodes for given ways 530 549 * @param Collection<Way> The list of ways which nodes are to be returned … … 538 557 return allNodes; 539 558 } 540 559 560 541 561 /** 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 562 * Gets all inner ways given all ways and outer ways. 563 * @param multigonWays 564 * @param outerWays 565 * @return list of inner ways. 566 */ 567 private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<Way> outerWays) { 568 ArrayList<Way> innerWays = new ArrayList<Way>(); 569 for(Way way: multigonWays) { 570 if (!outerWays.contains(way)) { 571 innerWays.add(way); 572 } 573 } 574 575 return innerWays; 576 } 577 578 579 /** 580 * Finds all ways for a given list of Ways that form the outer hull. 581 * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 582 * Prerequisites - the ways must not intersect and have common end nodes where they meet. 545 583 * @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 584 * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 548 585 */ 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())); 586 public static ArrayList<Way> findOuterWays(Collection<Way> multigonWays) { 587 588 //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 589 Way bestWay = null; 590 Node topNode = null; 591 int topIndex = 0; 592 double minLat = Double.POSITIVE_INFINITY; 593 594 for(Way way: multigonWays) { 595 for (int pos = 0; pos < way.getNodesCount(); pos ++){ 596 Node node = way.getNode(pos); 597 598 if (node.getCoor().lat() < minLat){ 599 minLat = node.getCoor().lat(); 600 bestWay = way; 601 topNode = node; 602 topIndex = pos; 603 } 555 604 } 605 } 556 606 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); 607 //get two final nodes from best way to mark as starting point and orientation. 608 Node headNode = null; 609 Node prevNode = null; 610 611 if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 612 { 613 //node is in split point 614 headNode = topNode; 615 //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 616 prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 617 } 618 else 619 { 620 //node is inside way - pick the clockwise going end. 621 Node prev = bestWay.getNode(topIndex - 1); 622 Node next = bestWay.getNode(topIndex + 1); 623 624 if (angleIsClockwise(prev, topNode, next)){ 625 headNode = bestWay.lastNode(); 626 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 627 } 628 else 629 { 630 headNode = bestWay.firstNode(); 631 prevNode = bestWay.getNode(1); 632 } 633 } 634 635 ArrayList<Way> outerWays = new ArrayList<Way>(); 636 637 //iterate till full circle is reached 638 while (true){ 639 640 bestWay = null; 641 Node bestWayNextNode = null; 642 boolean bestWayReverse = false; 643 644 for (Way way: multigonWays) 645 { 646 boolean wayReverse; 647 Node nextNode; 648 649 if (way.firstNode().equals(headNode)){ 650 nextNode = way.getNode(1); 651 wayReverse = false; 560 652 } 653 else if (way.lastNode().equals(headNode)) 654 { 655 nextNode = way.getNode(way.getNodesCount() - 2); 656 wayReverse = true; 657 } 658 else 659 { 660 //this way not adjacent to headNode 661 continue; 662 } 663 664 if (nextNode.equals(prevNode)) 665 { 666 //this is the path we came from - ignore it. 667 } 668 else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 669 { 670 //the new way is better 671 bestWay = way; 672 bestWayReverse = wayReverse; 673 bestWayNextNode = nextNode; 674 } 561 675 } 676 677 if (bestWay == null) 678 //this should not happen. Internal error here. 679 return null; 680 else if (outerWays.contains(bestWay)){ 681 //full circle reached, terminate. 682 break; 683 } 684 else 685 { 686 //add to outer ways, repeat. 687 outerWays.add(bestWay); 688 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 689 prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2); 690 } 562 691 } 563 692 564 return innerWays;693 return outerWays; 565 694 } 566 695 567 // Polygon only supports int coordinates, so convert them 568 private int latlonToXY(double val) { 569 return (int)Math.round(val*1000000); 696 /** 697 * Tests if given point is to the right side of path consisting of 3 points. 698 * @param lineP1 first point in path 699 * @param lineP2 second point in path 700 * @param lineP3 third point in path 701 * @param testPoint 702 * @return true if to the right side, false otherwise 703 */ 704 public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 705 { 706 boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 707 boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 708 boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 709 710 if (pathBendToRight) 711 return rightOfSeg1 && rightOfSeg2; 712 else 713 return !(!rightOfSeg1 && !rightOfSeg2); 570 714 } 571 715 572 716 /** 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 717 * This method tests if secondNode is clockwise to first node. 718 * @param commonNode starting point for both vectors 719 * @param firstNode first vector end node 720 * @param secondNode second vector end node 721 * @return true if first vector is clockwise before second vector. 577 722 */ 578 private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 579 for(Way way : w) { 580 if(!(way).containsNode(n)) { 723 public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 724 { 725 double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 726 double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 727 double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 728 double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 729 730 return dla1 * dlo2 - dlo1 * dla2 > 0; 731 } 732 733 734 /** 735 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 736 * @param polygonNodes list of nodes from polygon path. 737 * @param point the point to test 738 * @return true if the point is inside polygon. 739 * FIXME: this should probably be moved to tools.. 740 */ 741 public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 742 { 743 if (polygonNodes.size() < 3) 744 return false; 745 746 boolean inside = false; 747 Node p1, p2; 748 749 //iterate each side of the polygon, start with the last segment 750 Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 751 752 for(Node newPoint: polygonNodes) 753 { 754 //skip duplicate points 755 if (newPoint.equals(oldPoint)) { 581 756 continue; 582 757 } 583 if(!innerWays.contains(way)) { 584 innerWays.add(way); // Will need this later for multigons 758 759 //order points so p1.lat <= p2.lat; 760 if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 761 { 762 p1 = oldPoint; 763 p2 = newPoint; 585 764 } 765 else 766 { 767 p1 = newPoint; 768 p2 = oldPoint; 769 } 770 771 //test if the line is crossed and if so invert the inside flag. 772 if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 773 && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 774 < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 775 { 776 inside = !inside; 777 } 778 779 oldPoint = newPoint; 586 780 } 781 782 return inside; 587 783 } 588 784 785 786 787 589 788 /** 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 789 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 790 * @param Collection<Way> The list of outer ways that belong to that multigon. 593 791 * @return Way The newly created outer way 594 792 */ 595 private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) { 596 ArrayList<Way> join = new ArrayList<Way>(); 597 for(Way w: multigonWays) { 598 // Skip inner ways 599 if(innerWays.contains(w)) { 600 continue; 601 } 793 private Way joinOuterWays(ArrayList<Way> outerWays) { 602 794 603 if(w.getNodesCount() <= 2) {604 cmds.add(new DeleteCommand(w));605 } else {606 join.add(w);607 }608 }609 610 795 commitCommands(marktr("Join Areas: Remove Short Ways")); 611 Way joinedWay = joinWays( join);796 Way joinedWay = joinWays(outerWays); 612 797 if (joinedWay != null) 613 798 return closeWay(joinedWay); 614 799 else … … 980 1165 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 981 1166 setEnabled(selection != null && !selection.isEmpty()); 982 1167 } 983 } 1168 1169 } 1170 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 }