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

Last change on this file since 6959 was 6913, checked in by akks, 10 years ago

see #9832: move tags from ways to the multipolygons created by Shift-J

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