source: josm/trunk/src/org/openstreetmap/josm/actions/JoinAreasAction.java@ 6289

Last change on this file since 6289 was 6265, checked in by Don-vip, 11 years ago

Sonar/FindBugs:

  • Dodgy - Redundant nullcheck of value known to be non-null
  • Dodgy - Write to static field from instance method
  • Property svn:eol-style set to native
File size: 51.6 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.awt.geom.Area;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.LinkedHashSet;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.Map;
20import java.util.Set;
21import java.util.TreeMap;
22
23import javax.swing.JOptionPane;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
27import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
28import org.openstreetmap.josm.command.AddCommand;
29import org.openstreetmap.josm.command.ChangeCommand;
30import org.openstreetmap.josm.command.Command;
31import org.openstreetmap.josm.command.DeleteCommand;
32import org.openstreetmap.josm.command.SequenceCommand;
33import org.openstreetmap.josm.corrector.UserCancelException;
34import org.openstreetmap.josm.data.UndoRedoHandler;
35import org.openstreetmap.josm.data.coor.EastNorth;
36import org.openstreetmap.josm.data.osm.DataSet;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.NodePositionComparator;
39import org.openstreetmap.josm.data.osm.OsmPrimitive;
40import org.openstreetmap.josm.data.osm.Relation;
41import org.openstreetmap.josm.data.osm.RelationMember;
42import org.openstreetmap.josm.data.osm.TagCollection;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.gui.Notification;
45import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
46import org.openstreetmap.josm.tools.Geometry;
47import org.openstreetmap.josm.tools.Pair;
48import org.openstreetmap.josm.tools.Shortcut;
49
50/**
51 * Join Areas (i.e. closed ways and multipolygons)
52 */
53public class JoinAreasAction extends JosmAction {
54 // This will be used to commit commands and unite them into one large command sequence at the end
55 private LinkedList<Command> cmds = new LinkedList<Command>();
56 private int cmdsCount = 0;
57
58 /**
59 * This helper class describes join ares action result.
60 * @author viesturs
61 *
62 */
63 public static class JoinAreasResult {
64
65 public boolean hasChanges;
66
67 public List<Multipolygon> polygons;
68 }
69
70 public static class Multipolygon {
71 public Way outerWay;
72 public List<Way> innerWays;
73
74 public Relation relation;
75
76 public Multipolygon(Way way) {
77 outerWay = way;
78 innerWays = new ArrayList<Way>();
79 }
80 }
81
82 // HelperClass
83 // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
84 private static class RelationRole {
85 public final Relation rel;
86 public final String role;
87 public RelationRole(Relation rel, String role) {
88 this.rel = rel;
89 this.role = role;
90 }
91
92 @Override
93 public int hashCode() {
94 return rel.hashCode();
95 }
96
97 @Override
98 public boolean equals(Object other) {
99 if (!(other instanceof RelationRole)) return false;
100 RelationRole otherMember = (RelationRole) other;
101 return otherMember.role.equals(role) && otherMember.rel.equals(rel);
102 }
103 }
104
105
106 /**
107 * HelperClass - saves a way and the "inside" side.
108 *
109 * insideToTheLeft: if true left side is "in", false -right side is "in".
110 * Left and right are determined along the orientation of way.
111 */
112 public static class WayInPolygon {
113 public final Way way;
114 public boolean insideToTheRight;
115
116 public WayInPolygon(Way _way, boolean _insideRight) {
117 this.way = _way;
118 this.insideToTheRight = _insideRight;
119 }
120
121 @Override
122 public int hashCode() {
123 return way.hashCode();
124 }
125
126 @Override
127 public boolean equals(Object other) {
128 if (!(other instanceof WayInPolygon)) return false;
129 WayInPolygon otherMember = (WayInPolygon) other;
130 return otherMember.way.equals(this.way) && otherMember.insideToTheRight == this.insideToTheRight;
131 }
132 }
133
134 /**
135 * This helper class describes a polygon, assembled from several ways.
136 * @author viesturs
137 *
138 */
139 public static class AssembledPolygon {
140 public List<WayInPolygon> ways;
141
142 public AssembledPolygon(List<WayInPolygon> boundary) {
143 this.ways = boundary;
144 }
145
146 public List<Node> getNodes() {
147 List<Node> nodes = new ArrayList<Node>();
148 for (WayInPolygon way : this.ways) {
149 //do not add the last node as it will be repeated in the next way
150 if (way.insideToTheRight) {
151 for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
152 nodes.add(way.way.getNode(pos));
153 }
154 }
155 else {
156 for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
157 nodes.add(way.way.getNode(pos));
158 }
159 }
160 }
161
162 return nodes;
163 }
164 }
165
166 public static class AssembledMultipolygon {
167 public AssembledPolygon outerWay;
168 public List<AssembledPolygon> innerWays;
169
170 public AssembledMultipolygon(AssembledPolygon way) {
171 outerWay = way;
172 innerWays = new ArrayList<AssembledPolygon>();
173 }
174 }
175
176 /**
177 * This hepler class implements algorithm traversing trough connected ways.
178 * Assumes you are going in clockwise orientation.
179 * @author viesturs
180 *
181 */
182 private static class WayTraverser {
183
184 private Set<WayInPolygon> availableWays;
185 private WayInPolygon lastWay;
186 private boolean lastWayReverse;
187
188 public WayTraverser(Collection<WayInPolygon> ways) {
189
190 availableWays = new HashSet<WayInPolygon>(ways);
191 lastWay = null;
192 }
193
194 public void removeWays(Collection<WayInPolygon> ways) {
195 availableWays.removeAll(ways);
196 }
197
198 public boolean hasWays() {
199 return !availableWays.isEmpty();
200 }
201
202 public WayInPolygon startNewWay(WayInPolygon way) {
203 lastWay = way;
204 lastWayReverse = !lastWay.insideToTheRight;
205
206 return lastWay;
207 }
208
209 public WayInPolygon startNewWay() {
210 if (availableWays.isEmpty()) {
211 lastWay = null;
212 } else {
213 lastWay = availableWays.iterator().next();
214 lastWayReverse = !lastWay.insideToTheRight;
215 }
216
217 return lastWay;
218 }
219
220
221 public WayInPolygon advanceNextLeftmostWay() {
222 return advanceNextWay(false);
223 }
224
225 public WayInPolygon advanceNextRightmostWay() {
226 return advanceNextWay(true);
227 }
228
229 private WayInPolygon advanceNextWay(boolean rightmost) {
230
231 Node headNode = !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
232 Node prevNode = !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
233
234 //find best next way
235 WayInPolygon bestWay = null;
236 Node bestWayNextNode = null;
237 boolean bestWayReverse = false;
238
239 for (WayInPolygon way : availableWays) {
240 if (way.way.firstNode().equals(headNode)) {
241 //start adjacent to headNode
242 Node nextNode = way.way.getNode(1);
243
244 if (nextNode.equals(prevNode))
245 {
246 //this is the path we came from - ignore it.
247 }
248 else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
249 //the new way is better
250 bestWay = way;
251 bestWayReverse = false;
252 bestWayNextNode = nextNode;
253 }
254 }
255
256 if (way.way.lastNode().equals(headNode)) {
257 //end adjacent to headNode
258 Node nextNode = way.way.getNode(way.way.getNodesCount() - 2);
259
260 if (nextNode.equals(prevNode)) {
261 //this is the path we came from - ignore it.
262 }
263 else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
264 //the new way is better
265 bestWay = way;
266 bestWayReverse = true;
267 bestWayNextNode = nextNode;
268 }
269 }
270 }
271
272 lastWay = bestWay;
273 lastWayReverse = bestWayReverse;
274
275 return lastWay;
276 }
277
278 public boolean isLastWayInsideToTheRight() {
279 return lastWayReverse != lastWay.insideToTheRight;
280 }
281
282 public Node getLastWayStartNode() {
283 return lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
284 }
285
286 public Node getLastWayEndNode() {
287 return lastWayReverse ? lastWay.way.firstNode() : lastWay.way.lastNode();
288 }
289 }
290
291 /**
292 * Helper storage class for finding findOuterWays
293 * @author viesturs
294 */
295 static class PolygonLevel {
296 public final int level;
297 public final AssembledMultipolygon pol;
298
299 public PolygonLevel(AssembledMultipolygon _pol, int _level) {
300 pol = _pol;
301 level = _level;
302 }
303 }
304
305 // Adds the menu entry, Shortcuts, etc.
306 public JoinAreasAction() {
307 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
308 Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
309 KeyEvent.VK_J, Shortcut.SHIFT), true);
310 }
311
312 /**
313 * Gets called whenever the shortcut is pressed or the menu entry is selected
314 * Checks whether the selected objects are suitable to join and joins them if so
315 */
316 @Override
317 public void actionPerformed(ActionEvent e) {
318 LinkedList<Way> ways = new LinkedList<Way>(Main.main.getCurrentDataSet().getSelectedWays());
319
320 if (ways.isEmpty()) {
321 new Notification(
322 tr("Please select at least one closed way that should be joined."))
323 .setIcon(JOptionPane.INFORMATION_MESSAGE)
324 .show();
325 return;
326 }
327
328 List<Node> allNodes = new ArrayList<Node>();
329 for (Way way : ways) {
330 if (!way.isClosed()) {
331 new Notification(
332 tr("One of the selected ways is not closed and therefore cannot be joined."))
333 .setIcon(JOptionPane.INFORMATION_MESSAGE)
334 .show();
335 return;
336 }
337
338 allNodes.addAll(way.getNodes());
339 }
340
341 // TODO: Only display this warning when nodes outside dataSourceArea are deleted
342 Area dataSourceArea = Main.main.getCurrentDataSet().getDataSourceArea();
343 boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
344 trn("The selected way has nodes outside of the downloaded data region.",
345 "The selected ways have nodes outside of the downloaded data region.",
346 ways.size()) + "<br/>"
347 + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
348 + tr("Are you really sure to continue?")
349 + tr("Please abort if you are not sure"),
350 tr("The selected area is incomplete. Continue?"),
351 dataSourceArea, allNodes, null);
352 if(!ok) return;
353
354 //analyze multipolygon relations and collect all areas
355 List<Multipolygon> areas = collectMultipolygons(ways);
356
357 if (areas == null)
358 //too complex multipolygon relations found
359 return;
360
361 if (!testJoin(areas)) {
362 new Notification(
363 tr("No intersection found. Nothing was changed."))
364 .setIcon(JOptionPane.INFORMATION_MESSAGE)
365 .show();
366 return;
367 }
368
369 if (!resolveTagConflicts(areas))
370 return;
371 //user canceled, do nothing.
372
373 try {
374 JoinAreasResult result = joinAreas(areas);
375
376 if (result.hasChanges) {
377
378 List<Way> allWays = new ArrayList<Way>();
379 for (Multipolygon pol : result.polygons) {
380 allWays.add(pol.outerWay);
381 allWays.addAll(pol.innerWays);
382 }
383 DataSet ds = Main.main.getCurrentDataSet();
384 ds.setSelected(allWays);
385 Main.map.mapView.repaint();
386 } else {
387 new Notification(
388 tr("No intersection found. Nothing was changed."))
389 .setIcon(JOptionPane.INFORMATION_MESSAGE)
390 .show();
391 }
392 }
393 catch (UserCancelException exception) {
394 //revert changes
395 //FIXME: this is dirty hack
396 makeCommitsOneAction(tr("Reverting changes"));
397 Main.main.undoRedo.undo();
398 Main.main.undoRedo.redoCommands.clear();
399 }
400 }
401
402 /**
403 * Tests if the areas have some intersections to join.
404 * @param areas Areas to test
405 * @return @{code true} if areas are joinable
406 */
407 private boolean testJoin(List<Multipolygon> areas) {
408 List<Way> allStartingWays = new ArrayList<Way>();
409
410 for (Multipolygon area : areas) {
411 allStartingWays.add(area.outerWay);
412 allStartingWays.addAll(area.innerWays);
413 }
414
415 //find intersection points
416 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
417 return !nodes.isEmpty();
418 }
419
420 /**
421 * Will join two or more overlapping areas
422 * @param areas list of areas to join
423 * @return new area formed.
424 */
425 private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
426
427 JoinAreasResult result = new JoinAreasResult();
428 result.hasChanges = false;
429
430 List<Way> allStartingWays = new ArrayList<Way>();
431 List<Way> innerStartingWays = new ArrayList<Way>();
432 List<Way> outerStartingWays = new ArrayList<Way>();
433
434 for (Multipolygon area : areas) {
435 outerStartingWays.add(area.outerWay);
436 innerStartingWays.addAll(area.innerWays);
437 }
438
439 allStartingWays.addAll(innerStartingWays);
440 allStartingWays.addAll(outerStartingWays);
441
442 //first remove nodes in the same coordinate
443 boolean removedDuplicates = false;
444 removedDuplicates |= removeDuplicateNodes(allStartingWays);
445
446 if (removedDuplicates) {
447 result.hasChanges = true;
448 commitCommands(marktr("Removed duplicate nodes"));
449 }
450
451 //find intersection points
452 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
453
454 //no intersections, return.
455 if (nodes.isEmpty())
456 return result;
457 commitCommands(marktr("Added node on all intersections"));
458
459 ArrayList<RelationRole> relations = new ArrayList<RelationRole>();
460
461 // Remove ways from all relations so ways can be combined/split quietly
462 for (Way way : allStartingWays) {
463 relations.addAll(removeFromAllRelations(way));
464 }
465
466 // Don't warn now, because it will really look corrupted
467 boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
468
469 ArrayList<WayInPolygon> preparedWays = new ArrayList<WayInPolygon>();
470
471 for (Way way : outerStartingWays) {
472 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
473 preparedWays.addAll(markWayInsideSide(splitWays, false));
474 }
475
476 for (Way way : innerStartingWays) {
477 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
478 preparedWays.addAll(markWayInsideSide(splitWays, true));
479 }
480
481 // Find boundary ways
482 ArrayList<Way> discardedWays = new ArrayList<Way>();
483 List<AssembledPolygon> bounadries = findBoundaryPolygons(preparedWays, discardedWays);
484
485 //find polygons
486 List<AssembledMultipolygon> preparedPolygons = findPolygons(bounadries);
487
488
489 //assemble final polygons
490 List<Multipolygon> polygons = new ArrayList<Multipolygon>();
491 Set<Relation> relationsToDelete = new LinkedHashSet<Relation>();
492
493 for (AssembledMultipolygon pol : preparedPolygons) {
494
495 //create the new ways
496 Multipolygon resultPol = joinPolygon(pol);
497
498 //create multipolygon relation, if necessary.
499 RelationRole ownMultipolygonRelation = addOwnMultigonRelation(resultPol.innerWays, resultPol.outerWay);
500
501 //add back the original relations, merged with our new multipolygon relation
502 fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
503
504 //strip tags from inner ways
505 //TODO: preserve tags on existing inner ways
506 stripTags(resultPol.innerWays);
507
508 polygons.add(resultPol);
509 }
510
511 commitCommands(marktr("Assemble new polygons"));
512
513 for(Relation rel: relationsToDelete) {
514 cmds.add(new DeleteCommand(rel));
515 }
516
517 commitCommands(marktr("Delete relations"));
518
519 // Delete the discarded inner ways
520 if (!discardedWays.isEmpty()) {
521 Command deleteCmd = DeleteCommand.delete(Main.map.mapView.getEditLayer(), discardedWays, true);
522 if (deleteCmd != null) {
523 cmds.add(deleteCmd);
524 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
525 }
526 }
527
528 makeCommitsOneAction(marktr("Joined overlapping areas"));
529
530 if (warnAboutRelations) {
531 new Notification(
532 tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
533 .setIcon(JOptionPane.INFORMATION_MESSAGE)
534 .setDuration(Notification.TIME_LONG)
535 .show();
536 }
537
538 result.hasChanges = true;
539 result.polygons = polygons;
540 return result;
541 }
542
543 /**
544 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
545 * @param polygons ways to check
546 * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
547 */
548 private boolean resolveTagConflicts(List<Multipolygon> polygons) {
549
550 List<Way> ways = new ArrayList<Way>();
551
552 for (Multipolygon pol : polygons) {
553 ways.add(pol.outerWay);
554 ways.addAll(pol.innerWays);
555 }
556
557 if (ways.size() < 2) {
558 return true;
559 }
560
561 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
562 try {
563 cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
564 commitCommands(marktr("Fix tag conflicts"));
565 return true;
566 } catch (UserCancelException ex) {
567 return false;
568 }
569 }
570
571 /**
572 * This method removes duplicate points (if any) from the input way.
573 * @param ways the ways to process
574 * @return {@code true} if any changes where made
575 */
576 private boolean removeDuplicateNodes(List<Way> ways) {
577 //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
578
579 Map<Node, Node> nodeMap = new TreeMap<Node, Node>(new NodePositionComparator());
580 int totalNodesRemoved = 0;
581
582 for (Way way : ways) {
583 if (way.getNodes().size() < 2) {
584 continue;
585 }
586
587 int nodesRemoved = 0;
588 List<Node> newNodes = new ArrayList<Node>();
589 Node prevNode = null;
590
591 for (Node node : way.getNodes()) {
592 if (!nodeMap.containsKey(node)) {
593 //new node
594 nodeMap.put(node, node);
595
596 //avoid duplicate nodes
597 if (prevNode != node) {
598 newNodes.add(node);
599 } else {
600 nodesRemoved ++;
601 }
602 } else {
603 //node with same coordinates already exists, substitute with existing node
604 Node representator = nodeMap.get(node);
605
606 if (representator != node) {
607 nodesRemoved ++;
608 }
609
610 //avoid duplicate node
611 if (prevNode != representator) {
612 newNodes.add(representator);
613 }
614 }
615 prevNode = node;
616 }
617
618 if (nodesRemoved > 0) {
619
620 if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
621 newNodes.add(newNodes.get(0));
622 }
623
624 Way newWay=new Way(way);
625 newWay.setNodes(newNodes);
626 cmds.add(new ChangeCommand(way, newWay));
627 totalNodesRemoved += nodesRemoved;
628 }
629 }
630
631 return totalNodesRemoved > 0;
632 }
633
634 /**
635 * Commits the command list with a description
636 * @param description The description of what the commands do
637 */
638 private void commitCommands(String description) {
639 switch(cmds.size()) {
640 case 0:
641 return;
642 case 1:
643 Main.main.undoRedo.add(cmds.getFirst());
644 break;
645 default:
646 Command c = new SequenceCommand(tr(description), cmds);
647 Main.main.undoRedo.add(c);
648 break;
649 }
650
651 cmds.clear();
652 cmdsCount++;
653 }
654
655 /**
656 * This method analyzes the way and assigns each part what direction polygon "inside" is.
657 * @param parts the split parts of the way
658 * @param isInner - if true, reverts the direction (for multipolygon islands)
659 * @return list of parts, marked with the inside orientation.
660 */
661 private ArrayList<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
662
663 ArrayList<WayInPolygon> result = new ArrayList<WayInPolygon>();
664
665 //prepare prev and next maps
666 Map<Way, Way> nextWayMap = new HashMap<Way, Way>();
667 Map<Way, Way> prevWayMap = new HashMap<Way, Way>();
668
669 for (int pos = 0; pos < parts.size(); pos ++) {
670
671 if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
672 throw new RuntimeException("Way not circular");
673
674 nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
675 prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size()));
676 }
677
678 //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
679 Way topWay = null;
680 Node topNode = null;
681 int topIndex = 0;
682 double minY = Double.POSITIVE_INFINITY;
683
684 for (Way way : parts) {
685 for (int pos = 0; pos < way.getNodesCount(); pos ++) {
686 Node node = way.getNode(pos);
687
688 if (node.getEastNorth().getY() < minY) {
689 minY = node.getEastNorth().getY();
690 topWay = way;
691 topNode = node;
692 topIndex = pos;
693 }
694 }
695 }
696
697 //get the upper way and it's orientation.
698
699 boolean wayClockwise; // orientation of the top way.
700
701 if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) {
702 Node headNode = null; // the node at junction
703 Node prevNode = null; // last node from previous path
704 wayClockwise = false;
705
706 //node is in split point - find the outermost way from this point
707
708 headNode = topNode;
709 //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
710 prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
711
712 topWay = null;
713 wayClockwise = false;
714 Node bestWayNextNode = null;
715
716 for (Way way : parts) {
717 if (way.firstNode().equals(headNode)) {
718 Node nextNode = way.getNode(1);
719
720 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
721 //the new way is better
722 topWay = way;
723 wayClockwise = true;
724 bestWayNextNode = nextNode;
725 }
726 }
727
728 if (way.lastNode().equals(headNode)) {
729 //end adjacent to headNode
730 Node nextNode = way.getNode(way.getNodesCount() - 2);
731
732 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
733 //the new way is better
734 topWay = way;
735 wayClockwise = false;
736 bestWayNextNode = nextNode;
737 }
738 }
739 }
740 } else {
741 //node is inside way - pick the clockwise going end.
742 Node prev = topWay.getNode(topIndex - 1);
743 Node next = topWay.getNode(topIndex + 1);
744
745 //there will be no parallel segments in the middle of way, so all fine.
746 wayClockwise = Geometry.angleIsClockwise(prev, topNode, next);
747 }
748
749 Way curWay = topWay;
750 boolean curWayInsideToTheRight = wayClockwise ^ isInner;
751
752 //iterate till full circle is reached
753 while (true) {
754
755 //add cur way
756 WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight);
757 result.add(resultWay);
758
759 //process next way
760 Way nextWay = nextWayMap.get(curWay);
761 Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
762 Node headNode = curWay.lastNode();
763 Node nextNode = nextWay.getNode(1);
764
765 if (nextWay == topWay) {
766 //full loop traversed - all done.
767 break;
768 }
769
770 //find intersecting segments
771 // the intersections will look like this:
772 //
773 // ^
774 // |
775 // X wayBNode
776 // |
777 // wayB |
778 // |
779 // curWay | nextWay
780 //----X----------------->X----------------------X---->
781 // prevNode ^headNode nextNode
782 // |
783 // |
784 // wayA |
785 // |
786 // X wayANode
787 // |
788
789 int intersectionCount = 0;
790
791 for (Way wayA : parts) {
792
793 if (wayA == curWay) {
794 continue;
795 }
796
797 if (wayA.lastNode().equals(headNode)) {
798
799 Way wayB = nextWayMap.get(wayA);
800
801 //test if wayA is opposite wayB relative to curWay and nextWay
802
803 Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
804 Node wayBNode = wayB.getNode(1);
805
806 boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
807 boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
808
809 if (wayAToTheRight != wayBToTheRight) {
810 intersectionCount ++;
811 }
812 }
813 }
814
815 //if odd number of crossings, invert orientation
816 if (intersectionCount % 2 != 0) {
817 curWayInsideToTheRight = !curWayInsideToTheRight;
818 }
819
820 curWay = nextWay;
821 }
822
823 return result;
824 }
825
826 /**
827 * This is a method splits way into smaller parts, using the prepared nodes list as split points.
828 * Uses SplitWayAction.splitWay for the heavy lifting.
829 * @return list of split ways (or original ways if no splitting is done).
830 */
831 private ArrayList<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
832
833 ArrayList<Way> result = new ArrayList<Way>();
834 List<List<Node>> chunks = buildNodeChunks(way, nodes);
835
836 if (chunks.size() > 1) {
837 SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
838
839 //execute the command, we need the results
840 cmds.add(split.getCommand());
841 commitCommands(marktr("Split ways into fragments"));
842
843 result.add(split.getOriginalWay());
844 result.addAll(split.getNewWays());
845 } else {
846 //nothing to split
847 result.add(way);
848 }
849
850 return result;
851 }
852
853 /**
854 * Simple chunking version. Does not care about circular ways and result being
855 * proper, we will glue it all back together later on.
856 * @param way the way to chunk
857 * @param splitNodes the places where to cut.
858 * @return list of node paths to produce.
859 */
860 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
861 List<List<Node>> result = new ArrayList<List<Node>>();
862 List<Node> curList = new ArrayList<Node>();
863
864 for (Node node : way.getNodes()) {
865 curList.add(node);
866 if (curList.size() > 1 && splitNodes.contains(node)) {
867 result.add(curList);
868 curList = new ArrayList<Node>();
869 curList.add(node);
870 }
871 }
872
873 if (curList.size() > 1) {
874 result.add(curList);
875 }
876
877 return result;
878 }
879
880
881 /**
882 * This method finds which ways are outer and which are inner.
883 * @param boundaries list of joined boundaries to search in
884 * @return outer ways
885 */
886 private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
887
888 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
889 List<AssembledMultipolygon> result = new ArrayList<AssembledMultipolygon>();
890
891 //take every other level
892 for (PolygonLevel pol : list) {
893 if (pol.level % 2 == 0) {
894 result.add(pol.pol);
895 }
896 }
897
898 return result;
899 }
900
901 /**
902 * Collects outer way and corresponding inner ways from all boundaries.
903 * @param level depth level
904 * @param boundaryWays
905 * @return the outermostWay.
906 */
907 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
908
909 //TODO: bad performance for deep nestings...
910 List<PolygonLevel> result = new ArrayList<PolygonLevel>();
911
912 for (AssembledPolygon outerWay : boundaryWays) {
913
914 boolean outerGood = true;
915 List<AssembledPolygon> innerCandidates = new ArrayList<AssembledPolygon>();
916
917 for (AssembledPolygon innerWay : boundaryWays) {
918 if (innerWay == outerWay) {
919 continue;
920 }
921
922 if (wayInsideWay(outerWay, innerWay)) {
923 outerGood = false;
924 break;
925 } else if (wayInsideWay(innerWay, outerWay)) {
926 innerCandidates.add(innerWay);
927 }
928 }
929
930 if (!outerGood) {
931 continue;
932 }
933
934 //add new outer polygon
935 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
936 PolygonLevel polLev = new PolygonLevel(pol, level);
937
938 //process inner ways
939 if (!innerCandidates.isEmpty()) {
940 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
941 result.addAll(innerList);
942
943 for (PolygonLevel pl : innerList) {
944 if (pl.level == level + 1) {
945 pol.innerWays.add(pl.pol.outerWay);
946 }
947 }
948 }
949
950 result.add(polLev);
951 }
952
953 return result;
954 }
955
956 /**
957 * Finds all ways that form inner or outer boundaries.
958 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
959 * @param discardedResult this list is filled with ways that are to be discarded
960 * @return A list of ways that form the outer and inner boundaries of the multigon.
961 */
962 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, List<Way> discardedResult) {
963 //first find all discardable ways, by getting outer shells.
964 //this will produce incorrect boundaries in some cases, but second pass will fix it.
965
966 List<WayInPolygon> discardedWays = new ArrayList<WayInPolygon>();
967 Set<WayInPolygon> processedWays = new HashSet<WayInPolygon>();
968 WayTraverser traverser = new WayTraverser(multigonWays);
969
970 for (WayInPolygon startWay : multigonWays) {
971 if (processedWays.contains(startWay)) {
972 continue;
973 }
974
975 traverser.startNewWay(startWay);
976
977 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
978 WayInPolygon lastWay = startWay;
979
980 while (true) {
981 boundary.add(lastWay);
982
983 WayInPolygon bestWay = traverser.advanceNextLeftmostWay();
984 boolean wayInsideToTheRight = bestWay == null ? false : traverser.isLastWayInsideToTheRight();
985
986 if (bestWay == null || processedWays.contains(bestWay) || !wayInsideToTheRight) {
987 //bad segment chain - proceed to discard it
988 lastWay = null;
989 break;
990 } else if (boundary.contains(bestWay)) {
991 //traversed way found - close the way
992 lastWay = bestWay;
993 break;
994 } else {
995 //proceed to next segment
996 lastWay = bestWay;
997 }
998 }
999
1000 if (lastWay != null) {
1001 //way good
1002 processedWays.addAll(boundary);
1003
1004 //remove junk segments at the start
1005 while (boundary.get(0) != lastWay) {
1006 discardedWays.add(boundary.get(0));
1007 boundary.remove(0);
1008 }
1009 } else {
1010 //way bad
1011 discardedWays.addAll(boundary);
1012 processedWays.addAll(boundary);
1013 }
1014 }
1015
1016 //now we have removed junk segments, collect the real result ways
1017
1018 traverser.removeWays(discardedWays);
1019
1020 List<AssembledPolygon> result = new ArrayList<AssembledPolygon>();
1021
1022 while (traverser.hasWays()) {
1023
1024 WayInPolygon startWay = traverser.startNewWay();
1025 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1026 WayInPolygon curWay = startWay;
1027
1028 do {
1029 boundary.add(curWay);
1030 curWay = traverser.advanceNextRightmostWay();
1031
1032 //should not happen
1033 if (curWay == null || !traverser.isLastWayInsideToTheRight())
1034 throw new RuntimeException("Join areas internal error.");
1035
1036 } while (curWay != startWay);
1037
1038 //build result
1039 traverser.removeWays(boundary);
1040 result.add(new AssembledPolygon(boundary));
1041 }
1042
1043 for (WayInPolygon way : discardedWays) {
1044 discardedResult.add(way.way);
1045 }
1046
1047 //split inner polygons that have several touching parts.
1048 result = fixTouchingPolygons(result);
1049
1050 return result;
1051 }
1052
1053 /**
1054 * This method checks if polygons have several touching parts and splits them in several polygons.
1055 * @param polygons the polygons to process.
1056 */
1057 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons)
1058 {
1059 List<AssembledPolygon> newPolygons = new ArrayList<AssembledPolygon>();
1060
1061 for (AssembledPolygon innerPart : polygons) {
1062 WayTraverser traverser = new WayTraverser(innerPart.ways);
1063
1064 while (traverser.hasWays()) {
1065
1066 WayInPolygon startWay = traverser.startNewWay();
1067 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1068 WayInPolygon curWay = startWay;
1069
1070 Node startNode = traverser.getLastWayStartNode();
1071 boundary.add(curWay);
1072
1073 while (startNode != traverser.getLastWayEndNode()) {
1074 curWay = traverser.advanceNextLeftmostWay();
1075 boundary.add(curWay);
1076
1077 //should not happen
1078 if (curWay == null || !traverser.isLastWayInsideToTheRight())
1079 throw new RuntimeException("Join areas internal error.");
1080 }
1081
1082 //build result
1083 traverser.removeWays(boundary);
1084 newPolygons.add(new AssembledPolygon(boundary));
1085 }
1086 }
1087
1088 return newPolygons;
1089 }
1090
1091 /**
1092 * Tests if way is inside other way
1093 * @param outside outer polygon description
1094 * @param inside inner polygon description
1095 * @return {@code true} if inner is inside outer
1096 */
1097 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1098 Set<Node> outsideNodes = new HashSet<Node>(outside.getNodes());
1099 List<Node> insideNodes = inside.getNodes();
1100
1101 for (Node insideNode : insideNodes) {
1102
1103 if (!outsideNodes.contains(insideNode))
1104 //simply test the one node
1105 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1106 }
1107
1108 //all nodes shared.
1109 return false;
1110 }
1111
1112 /**
1113 * Joins the lists of ways.
1114 * @param polygon The list of outer ways that belong to that multigon.
1115 * @return The newly created outer way
1116 */
1117 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1118 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1119
1120 for (AssembledPolygon pol : polygon.innerWays) {
1121 result.innerWays.add(joinWays(pol.ways));
1122 }
1123
1124 return result;
1125 }
1126
1127 /**
1128 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1129 * @param ways The list of outer ways that belong to that multigon.
1130 * @return The newly created outer way
1131 */
1132 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1133
1134 //leave original orientation, if all paths are reverse.
1135 boolean allReverse = true;
1136 for (WayInPolygon way : ways) {
1137 allReverse &= !way.insideToTheRight;
1138 }
1139
1140 if (allReverse) {
1141 for (WayInPolygon way : ways) {
1142 way.insideToTheRight = !way.insideToTheRight;
1143 }
1144 }
1145
1146 Way joinedWay = joinOrientedWays(ways);
1147
1148 //should not happen
1149 if (joinedWay == null || !joinedWay.isClosed())
1150 throw new RuntimeException("Join areas internal error.");
1151
1152 return joinedWay;
1153 }
1154
1155 /**
1156 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1157 * @param ways The list of ways to join and reverse
1158 * @return The newly created way
1159 */
1160 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException{
1161 if (ways.size() < 2)
1162 return ways.get(0).way;
1163
1164 // This will turn ways so all of them point in the same direction and CombineAction won't bug
1165 // the user about this.
1166
1167 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1168 List<Way> actionWays = new ArrayList<Way>(ways.size());
1169
1170 for (WayInPolygon way : ways) {
1171 actionWays.add(way.way);
1172
1173 if (!way.insideToTheRight) {
1174 ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1175 Main.main.undoRedo.add(res.getReverseCommand());
1176 cmdsCount++;
1177 }
1178 }
1179
1180 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1181
1182 Main.main.undoRedo.add(result.b);
1183 cmdsCount ++;
1184
1185 return result.a;
1186 }
1187
1188 /**
1189 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1190 * @param selectedWays the selected ways
1191 * @return list of polygons, or null if too complex relation encountered.
1192 */
1193 private List<Multipolygon> collectMultipolygons(List<Way> selectedWays) {
1194
1195 List<Multipolygon> result = new ArrayList<Multipolygon>();
1196
1197 //prepare the lists, to minimize memory allocation.
1198 List<Way> outerWays = new ArrayList<Way>();
1199 List<Way> innerWays = new ArrayList<Way>();
1200
1201 Set<Way> processedOuterWays = new LinkedHashSet<Way>();
1202 Set<Way> processedInnerWays = new LinkedHashSet<Way>();
1203
1204 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1205 if (r.isDeleted() || !r.isMultipolygon()) {
1206 continue;
1207 }
1208
1209 boolean hasKnownOuter = false;
1210 outerWays.clear();
1211 innerWays.clear();
1212
1213 for (RelationMember rm : r.getMembers()) {
1214 if (rm.getRole().equalsIgnoreCase("outer")) {
1215 outerWays.add(rm.getWay());
1216 hasKnownOuter |= selectedWays.contains(rm.getWay());
1217 }
1218 else if (rm.getRole().equalsIgnoreCase("inner")) {
1219 innerWays.add(rm.getWay());
1220 }
1221 }
1222
1223 if (!hasKnownOuter) {
1224 continue;
1225 }
1226
1227 if (outerWays.size() > 1) {
1228 new Notification(
1229 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
1230 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1231 .show();
1232 return null;
1233 }
1234
1235 Way outerWay = outerWays.get(0);
1236
1237 //retain only selected inner ways
1238 innerWays.retainAll(selectedWays);
1239
1240 if (processedOuterWays.contains(outerWay)) {
1241 new Notification(
1242 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
1243 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1244 .show();
1245 return null;
1246 }
1247
1248 if (processedInnerWays.contains(outerWay)) {
1249 new Notification(
1250 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1251 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1252 .show();
1253 return null;
1254 }
1255
1256 for (Way way :innerWays)
1257 {
1258 if (processedOuterWays.contains(way)) {
1259 new Notification(
1260 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1261 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1262 .show();
1263 return null;
1264 }
1265
1266 if (processedInnerWays.contains(way)) {
1267 new Notification(
1268 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
1269 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1270 .show();
1271 return null;
1272 }
1273 }
1274
1275 processedOuterWays.add(outerWay);
1276 processedInnerWays.addAll(innerWays);
1277
1278 Multipolygon pol = new Multipolygon(outerWay);
1279 pol.innerWays.addAll(innerWays);
1280 pol.relation = r;
1281
1282 result.add(pol);
1283 }
1284
1285 //add remaining ways, not in relations
1286 for (Way way : selectedWays) {
1287 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1288 continue;
1289 }
1290
1291 result.add(new Multipolygon(way));
1292 }
1293
1294 return result;
1295 }
1296
1297 /**
1298 * This method filters the list of relations that form the multipolygons.
1299 * @param relations all relations
1300 * @param polygons polygons for filtering
1301 * @return relations which don't form the polygons
1302 */
1303 private List<Relation> filterOwnMultipolygonRelations(Collection<Relation> relations, List<Multipolygon> polygons) {
1304
1305 List<Relation> relationsToRemove = new ArrayList<Relation>();
1306
1307 for (Multipolygon m : polygons) {
1308 if (m.relation != null) {
1309 relationsToRemove.add(m.relation);
1310 }
1311 }
1312
1313 List<Relation> result = new ArrayList<Relation>();
1314
1315 result.addAll(relations);
1316 result.removeAll(relationsToRemove);
1317 return result;
1318 }
1319
1320 /**
1321 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1322 * @param inner List of already closed inner ways
1323 * @param outer The outer way
1324 * @return The list of relation with roles to add own relation to
1325 */
1326 private RelationRole addOwnMultigonRelation(Collection<Way> inner, Way outer) {
1327 if (inner.isEmpty()) return null;
1328 // Create new multipolygon relation and add all inner ways to it
1329 Relation newRel = new Relation();
1330 newRel.put("type", "multipolygon");
1331 for (Way w : inner) {
1332 newRel.addMember(new RelationMember("inner", w));
1333 }
1334 cmds.add(new AddCommand(newRel));
1335
1336 // We don't add outer to the relation because it will be handed to fixRelations()
1337 // which will then do the remaining work.
1338 return new RelationRole(newRel, "outer");
1339 }
1340
1341 /**
1342 * Removes a given OsmPrimitive from all relations
1343 * @param osm Element to remove from all relations
1344 * @return List of relations with roles the primitives was part of
1345 */
1346 private ArrayList<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1347 ArrayList<RelationRole> result = new ArrayList<RelationRole>();
1348
1349 for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
1350 if (r.isDeleted()) {
1351 continue;
1352 }
1353 for (RelationMember rm : r.getMembers()) {
1354 if (rm.getMember() != osm) {
1355 continue;
1356 }
1357
1358 Relation newRel = new Relation(r);
1359 List<RelationMember> members = newRel.getMembers();
1360 members.remove(rm);
1361 newRel.setMembers(members);
1362
1363 cmds.add(new ChangeCommand(r, newRel));
1364 RelationRole saverel = new RelationRole(r, rm.getRole());
1365 if (!result.contains(saverel)) {
1366 result.add(saverel);
1367 }
1368 break;
1369 }
1370 }
1371
1372 commitCommands(marktr("Removed Element from Relations"));
1373 return result;
1374 }
1375
1376 /**
1377 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1378 * relations where the joined areas were in "outer" role a new relation is created instead with all
1379 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1380 * @param rels List of relations with roles the (original) ways were part of
1381 * @param outer The newly created outer area/way
1382 * @param ownMultipol elements to directly add as outer
1383 * @param relationsToDelete set of relations to delete.
1384 */
1385 private void fixRelations(ArrayList<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1386 ArrayList<RelationRole> multiouters = new ArrayList<RelationRole>();
1387
1388 if (ownMultipol != null){
1389 multiouters.add(ownMultipol);
1390 }
1391
1392 for (RelationRole r : rels) {
1393 if (r.rel.isMultipolygon() && r.role.equalsIgnoreCase("outer")) {
1394 multiouters.add(r);
1395 continue;
1396 }
1397 // Add it back!
1398 Relation newRel = new Relation(r.rel);
1399 newRel.addMember(new RelationMember(r.role, outer));
1400 cmds.add(new ChangeCommand(r.rel, newRel));
1401 }
1402
1403 Relation newRel;
1404 switch (multiouters.size()) {
1405 case 0:
1406 return;
1407 case 1:
1408 // Found only one to be part of a multipolygon relation, so just add it back as well
1409 newRel = new Relation(multiouters.get(0).rel);
1410 newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1411 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1412 return;
1413 default:
1414 // Create a new relation with all previous members and (Way)outer as outer.
1415 newRel = new Relation();
1416 for (RelationRole r : multiouters) {
1417 // Add members
1418 for (RelationMember rm : r.rel.getMembers())
1419 if (!newRel.getMembers().contains(rm)) {
1420 newRel.addMember(rm);
1421 }
1422 // Add tags
1423 for (String key : r.rel.keySet()) {
1424 newRel.put(key, r.rel.get(key));
1425 }
1426 // Delete old relation
1427 relationsToDelete.add(r.rel);
1428 }
1429 newRel.addMember(new RelationMember("outer", outer));
1430 cmds.add(new AddCommand(newRel));
1431 }
1432 }
1433
1434 /**
1435 * Remove all tags from the all the way
1436 * @param ways The List of Ways to remove all tags from
1437 */
1438 private void stripTags(Collection<Way> ways) {
1439 for (Way w : ways) {
1440 stripTags(w);
1441 }
1442 /* I18N: current action printed in status display */
1443 commitCommands(marktr("Remove tags from inner ways"));
1444 }
1445
1446 /**
1447 * Remove all tags from the way
1448 * @param x The Way to remove all tags from
1449 */
1450 private void stripTags(Way x) {
1451 Way y = new Way(x);
1452 for (String key : x.keySet()) {
1453 y.remove(key);
1454 }
1455 cmds.add(new ChangeCommand(x, y));
1456 }
1457
1458 /**
1459 * Takes the last cmdsCount actions back and combines them into a single action
1460 * (for when the user wants to undo the join action)
1461 * @param message The commit message to display
1462 */
1463 private void makeCommitsOneAction(String message) {
1464 UndoRedoHandler ur = Main.main.undoRedo;
1465 cmds.clear();
1466 int i = Math.max(ur.commands.size() - cmdsCount, 0);
1467 for (; i < ur.commands.size(); i++) {
1468 cmds.add(ur.commands.get(i));
1469 }
1470
1471 for (i = 0; i < cmds.size(); i++) {
1472 ur.undo();
1473 }
1474
1475 commitCommands(message == null ? marktr("Join Areas Function") : message);
1476 cmdsCount = 0;
1477 }
1478
1479 @Override
1480 protected void updateEnabledState() {
1481 if (getCurrentDataSet() == null) {
1482 setEnabled(false);
1483 } else {
1484 updateEnabledState(getCurrentDataSet().getSelected());
1485 }
1486 }
1487
1488 @Override
1489 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1490 setEnabled(selection != null && !selection.isEmpty());
1491 }
1492}
Note: See TracBrowser for help on using the repository browser.