Ticket #5179: osm-join-areas-4.patch
File osm-join-areas-4.patch, 24.7 KB (added by , 14 years ago) |
---|
-
src/org/openstreetmap/josm/actions/CombineWayAction.java
14 14 import java.util.Collections; 15 15 import java.util.HashMap; 16 16 import java.util.HashSet; 17 import java.util.LinkedHashMap; 18 import java.util.LinkedHashSet; 17 19 import java.util.LinkedList; 18 20 import java.util.List; 19 21 import java.util.Set; … … 112 114 if (ways == null || ways.isEmpty()) 113 115 return null; 114 116 ways.remove(null); // just in case - remove all null ways from the collection 115 ways = new HashSet<Way>(ways); // remove duplicates116 117 118 // remove duplicates, preserving order 119 ways = new LinkedHashSet<Way>(ways); 120 117 121 // try to build a new way which includes all the combined 118 122 // ways 119 123 // … … 473 477 } 474 478 475 479 protected void prepare() { 476 Set<NodePair> undirectedEdges = new HashSet<NodePair>();477 successors = new HashMap<Node, List<NodePair>>();478 predecessors = new HashMap<Node, List<NodePair>>();480 Set<NodePair> undirectedEdges = new LinkedHashSet<NodePair>(); 481 successors = new LinkedHashMap<Node, List<NodePair>>(); 482 predecessors = new LinkedHashMap<Node, List<NodePair>>(); 479 483 480 484 for (NodePair pair: edges) { 481 485 if (!undirectedEdges.contains(pair) && ! undirectedEdges.contains(pair.swap())) { … … 488 492 } 489 493 490 494 public NodeGraph() { 491 edges = new HashSet<NodePair>();495 edges = new LinkedHashSet<NodePair>(); 492 496 } 493 497 494 498 public void add(NodePair pair) { … … 513 517 } 514 518 515 519 protected Set<Node> getTerminalNodes() { 516 Set<Node> ret = new HashSet<Node>();520 Set<Node> ret = new LinkedHashSet<Node>(); 517 521 for (Node n: getNodes()) { 518 522 if (isTerminalNode(n)) { 519 523 ret.add(n); … … 523 527 } 524 528 525 529 protected Set<Node> getNodes(Stack<NodePair> pairs) { 526 HashSet<Node> nodes = new HashSet<Node>();530 HashSet<Node> nodes = new LinkedHashSet<Node>(); 527 531 for (NodePair pair: pairs) { 528 532 nodes.add(pair.getA()); 529 533 nodes.add(pair.getB()); … … 543 547 } 544 548 545 549 protected Set<Node> getNodes() { 546 Set<Node> nodes = new HashSet<Node>();550 Set<Node> nodes = new LinkedHashSet<Node>(); 547 551 for (NodePair pair: edges) { 548 552 nodes.add(pair.getA()); 549 553 nodes.add(pair.getB()); -
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; … … 15 14 import java.util.Collection; 16 15 import java.util.Collections; 17 16 import java.util.HashMap; 17 import java.util.HashSet; 18 18 import java.util.LinkedList; 19 19 import java.util.List; 20 20 import java.util.Map; … … 30 30 import javax.swing.JPanel; 31 31 32 32 import org.openstreetmap.josm.Main; 33 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 33 34 import org.openstreetmap.josm.command.AddCommand; 34 35 import org.openstreetmap.josm.command.ChangeCommand; 35 36 import org.openstreetmap.josm.command.Command; … … 109 110 } 110 111 } 111 112 113 //HelperClass 114 //saves a way and it's orientation. 115 private static class WayInPath{ 116 public final Way way; 117 public final boolean reverse; 118 119 public WayInPath(Way _way, boolean _reverse){ 120 this.way = _way; 121 this.reverse = _reverse; 122 } 123 124 } 125 112 126 // Adds the menu entry, Shortcuts, etc. 113 127 public JoinAreasAction() { 114 128 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), … … 185 199 if(!same) { 186 200 int i = 0; 187 201 if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 202 203 //join each area with itself, fixing self-crossings. 188 204 if(joinAreas(a, a)) { 189 205 ++i; 190 206 } … … 195 211 cmdsCount = i; 196 212 } 197 213 198 ArrayList< OsmPrimitive> nodes = addIntersections(a, b);214 ArrayList<Node> nodes = addIntersections(a, b); 199 215 if(nodes.size() == 0) return hadChanges; 200 216 commitCommands(marktr("Added node on all intersections")); 201 217 … … 208 224 // Don't warn now, because it will really look corrupted 209 225 boolean warnAboutRelations = relations.size() > 0; 210 226 211 Collection<Way> allWays = splitWaysOnNodes(a, b, nodes);227 ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes); 212 228 213 // Find all nodes andinner ways save them to a list214 Collection<Node> allNodes = getNodesFromWays(allWays);215 Collection<Way> innerWays = findInnerWays(allWays, allNodes);229 // Find inner ways save them to a list 230 ArrayList<WayInPath> outerWays = findOuterWays(allWays); 231 ArrayList<Way> innerWays = findInnerWays(allWays, outerWays); 216 232 217 233 // Join outer ways 218 Way outerWay = joinOuterWays( allWays, innerWays);234 Way outerWay = joinOuterWays(outerWays); 219 235 if (outerWay == null) 220 236 return true; 221 237 … … 330 346 * @param Way Second way 331 347 * @return ArrayList<OsmPrimitive> List of new nodes 332 348 */ 333 private ArrayList< OsmPrimitive> addIntersections(Way a, Way b) {349 private ArrayList<Node> addIntersections(Way a, Way b) { 334 350 boolean same = a.equals(b); 335 351 int nodesSizeA = a.getNodesCount(); 336 352 int nodesSizeB = b.getNodesCount(); 337 353 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>(); 354 ArrayList<Node> nodes = new ArrayList<Node>(); 341 355 ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>(); 342 356 ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>(); 343 357 … … 488 502 } 489 503 490 504 /** 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. 505 * This is a method splits ways into smaller parts, using the prepared nodes list as split points. 506 * Uses SplitWayAction.splitWay for the heavy lifting. 507 * @return list of split ways (or original ways if no splitting is done). 493 508 */ 494 private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) { 495 ArrayList<Way> ways = new ArrayList<Way>(); 509 private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) { 510 511 ArrayList<Way> result = new ArrayList<Way>(); 512 List<Way> ways = new ArrayList<Way>(); 496 513 ways.add(a); 497 if(!a.equals(b)) { 498 ways.add(b); 514 ways.add(b); 515 516 for (Way way: ways){ 517 List<List<Node>> chunks = buildNodeChunks(way, nodes); 518 SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 519 520 //execute the command, we need the results 521 Main.main.undoRedo.add(split.getCommand()); 522 cmdsCount ++; 523 524 result.add(split.getOriginalWay()); 525 result.addAll(split.getNewWays()); 499 526 } 500 527 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); 528 return result; 511 529 } 512 530 531 513 532 /** 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 533 * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on. 534 * @param way the way to chunk 535 * @param splitNodes the places where to cut. 536 * @return list of node segments to produce. 517 537 */ 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); 538 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) 539 { 540 List<List<Node>> result = new ArrayList<List<Node>>(); 541 List<Node> curList = new ArrayList<Node>(); 542 543 for(Node node: way.getNodes()){ 544 curList.add(node); 545 if (curList.size() > 1 && splitNodes.contains(node)){ 546 result.add(curList); 547 curList = new ArrayList<Node>(); 548 curList.add(node); 523 549 } 524 550 } 551 552 if (curList.size() > 1) 553 { 554 result.add(curList); 555 } 556 525 557 return result; 526 558 } 527 559 560 528 561 /** 529 562 * Returns all nodes for given ways 530 563 * @param Collection<Way> The list of ways which nodes are to be returned … … 538 571 return allNodes; 539 572 } 540 573 574 541 575 /** 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 576 * Gets all inner ways given all ways and outer ways. 577 * @param multigonWays 578 * @param outerWays 579 * @return list of inner ways. 580 */ 581 private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) { 582 ArrayList<Way> innerWays = new ArrayList<Way>(); 583 Set<Way> outerSet = new HashSet<Way>(); 584 585 for(WayInPath w: outerWays) { 586 outerSet.add(w.way); 587 } 588 589 for(Way way: multigonWays) { 590 if (!outerSet.contains(way)) { 591 innerWays.add(way); 592 } 593 } 594 595 return innerWays; 596 } 597 598 599 /** 600 * Finds all ways for a given list of Ways that form the outer hull. 601 * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 602 * Prerequisites - the ways must not intersect and have common end nodes where they meet. 545 603 * @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 604 * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 548 605 */ 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())); 606 public static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) { 607 608 //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 609 Way bestWay = null; 610 Node topNode = null; 611 int topIndex = 0; 612 double minLat = Double.POSITIVE_INFINITY; 613 614 for(Way way: multigonWays) { 615 for (int pos = 0; pos < way.getNodesCount(); pos ++){ 616 Node node = way.getNode(pos); 617 618 if (node.getCoor().lat() < minLat){ 619 minLat = node.getCoor().lat(); 620 bestWay = way; 621 topNode = node; 622 topIndex = pos; 623 } 555 624 } 625 } 556 626 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); 627 //get two final nodes from best way to mark as starting point and orientation. 628 Node headNode = null; 629 Node prevNode = null; 630 631 if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 632 { 633 //node is in split point 634 headNode = topNode; 635 //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 636 prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 637 } 638 else 639 { 640 //node is inside way - pick the clockwise going end. 641 Node prev = bestWay.getNode(topIndex - 1); 642 Node next = bestWay.getNode(topIndex + 1); 643 644 if (angleIsClockwise(prev, topNode, next)){ 645 headNode = bestWay.lastNode(); 646 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 647 } 648 else 649 { 650 headNode = bestWay.firstNode(); 651 prevNode = bestWay.getNode(1); 652 } 653 } 654 655 Set<Way> outerWays = new HashSet<Way>(); 656 ArrayList<WayInPath> result = new ArrayList<WayInPath>(); 657 658 //iterate till full circle is reached 659 while (true){ 660 661 bestWay = null; 662 Node bestWayNextNode = null; 663 boolean bestWayReverse = false; 664 665 for (Way way: multigonWays) 666 { 667 boolean wayReverse; 668 Node nextNode; 669 670 if (way.firstNode().equals(headNode)){ 671 nextNode = way.getNode(1); 672 wayReverse = false; 560 673 } 674 else if (way.lastNode().equals(headNode)) 675 { 676 nextNode = way.getNode(way.getNodesCount() - 2); 677 wayReverse = true; 678 } 679 else 680 { 681 //this way not adjacent to headNode 682 continue; 683 } 684 685 if (nextNode.equals(prevNode)) 686 { 687 //this is the path we came from - ignore it. 688 } 689 else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 690 { 691 //the new way is better 692 bestWay = way; 693 bestWayReverse = wayReverse; 694 bestWayNextNode = nextNode; 695 } 561 696 } 697 698 if (bestWay == null) 699 //this should not happen. Internal error here. 700 return null; 701 else if (outerWays.contains(bestWay)){ 702 //full circle reached, terminate. 703 break; 704 } 705 else 706 { 707 //add to outer ways, repeat. 708 outerWays.add(bestWay); 709 result.add(new WayInPath(bestWay, bestWayReverse)); 710 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 711 prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2); 712 } 562 713 } 563 714 564 return innerWays;715 return result; 565 716 } 566 717 567 // Polygon only supports int coordinates, so convert them 568 private int latlonToXY(double val) { 569 return (int)Math.round(val*1000000); 718 /** 719 * Tests if given point is to the right side of path consisting of 3 points. 720 * @param lineP1 first point in path 721 * @param lineP2 second point in path 722 * @param lineP3 third point in path 723 * @param testPoint 724 * @return true if to the right side, false otherwise 725 */ 726 public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 727 { 728 boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 729 boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 730 boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 731 732 if (pathBendToRight) 733 return rightOfSeg1 && rightOfSeg2; 734 else 735 return !(!rightOfSeg1 && !rightOfSeg2); 570 736 } 571 737 572 738 /** 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 739 * This method tests if secondNode is clockwise to first node. 740 * @param commonNode starting point for both vectors 741 * @param firstNode first vector end node 742 * @param secondNode second vector end node 743 * @return true if first vector is clockwise before second vector. 577 744 */ 578 private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 579 for(Way way : w) { 580 if(!(way).containsNode(n)) { 745 public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 746 { 747 double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 748 double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 749 double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 750 double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 751 752 return dla1 * dlo2 - dlo1 * dla2 > 0; 753 } 754 755 756 /** 757 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 758 * @param polygonNodes list of nodes from polygon path. 759 * @param point the point to test 760 * @return true if the point is inside polygon. 761 * FIXME: this should probably be moved to tools.. 762 */ 763 public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 764 { 765 if (polygonNodes.size() < 3) 766 return false; 767 768 boolean inside = false; 769 Node p1, p2; 770 771 //iterate each side of the polygon, start with the last segment 772 Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 773 774 for(Node newPoint: polygonNodes) 775 { 776 //skip duplicate points 777 if (newPoint.equals(oldPoint)) { 581 778 continue; 582 779 } 583 if(!innerWays.contains(way)) { 584 innerWays.add(way); // Will need this later for multigons 780 781 //order points so p1.lat <= p2.lat; 782 if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 783 { 784 p1 = oldPoint; 785 p2 = newPoint; 585 786 } 787 else 788 { 789 p1 = newPoint; 790 p2 = oldPoint; 791 } 792 793 //test if the line is crossed and if so invert the inside flag. 794 if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 795 && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 796 < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 797 { 798 inside = !inside; 799 } 800 801 oldPoint = newPoint; 586 802 } 803 804 return inside; 587 805 } 588 806 807 808 809 589 810 /** 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 811 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 812 * @param Collection<Way> The list of outer ways that belong to that multigon. 593 813 * @return Way The newly created outer way 594 814 */ 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 } 815 private Way joinOuterWays(ArrayList<WayInPath> outerWays) { 602 816 603 if(w.getNodesCount() <= 2) {604 cmds.add(new DeleteCommand(w));605 } else {606 join.add(w);607 }608 }609 610 817 commitCommands(marktr("Join Areas: Remove Short Ways")); 611 Way joinedWay = join Ways(join);818 Way joinedWay = joinOrientedWays(outerWays); 612 819 if (joinedWay != null) 613 820 return closeWay(joinedWay); 614 821 else … … 632 839 } 633 840 634 841 /** 842 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 843 * @param ArrayList<Way> The list of ways to join and reverse 844 * @return Way The newly created way 845 */ 846 private Way joinOrientedWays(ArrayList<WayInPath> ways) { 847 if(ways.size() < 2) return ways.get(0).way; 848 849 // This will turn ways so all of them point in the same direction and CombineAction won't bug 850 // the user about this. 851 852 List<Way> actionWays = new ArrayList<Way>(ways.size()); 853 854 for(WayInPath way : ways) { 855 actionWays.add(way.way); 856 857 if (way.reverse) 858 { 859 Main.main.getCurrentDataSet().setSelected(way.way); 860 new ReverseWayAction().actionPerformed(null); 861 cmdsCount++; 862 } 863 } 864 865 Way result = new CombineWayAction().combineWays(actionWays); 866 867 if(result != null) { 868 cmdsCount++; 869 } 870 return result; 871 } 872 873 /** 635 874 * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former) 636 875 * @param ArrayList<Way> The list of ways to join 637 876 * @return Way The newly created way … … 661 900 return a; 662 901 } 663 902 903 664 904 /** 665 905 * Finds all ways that may be part of a multipolygon relation and removes them from the given list. 666 906 * It will automatically combine "good" ways … … 980 1220 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 981 1221 setEnabled(selection != null && !selection.isEmpty()); 982 1222 } 983 } 1223 1224 } 1225 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 }