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

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

fix #8692 - Handle null DeleteCommands in JoinAreaAction and ImproveWayAccuracyAction

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