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

Last change on this file since 6162 was 6130, checked in by bastiK, 11 years ago

see #6963 - converted popups to notifications for all actions in the Tools menu (TODO: Simplify Way)

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