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

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

fix problem with making multisplitpanelayout persistent (avoid loop reference); javadoc

  • Property svn:eol-style set to native
File size: 50.3 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/**
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 /**
60 * This helper class describes join ares action result.
61 * @author viesturs
62 *
63 */
64 public static class JoinAreasResult {
65
66 public boolean mergeSuccessful;
67 public boolean hasChanges;
68 public boolean hasRelationProblems;
69
70 public List<Multipolygon> polygons;
71 }
72
73 public static class Multipolygon {
74 public Way outerWay;
75 public List<Way> innerWays;
76
77 public Relation relation;
78
79 public Multipolygon(Way way) {
80 outerWay = way;
81 innerWays = new ArrayList<Way>();
82 }
83 }
84
85 // HelperClass
86 // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
87 private static class RelationRole {
88 public final Relation rel;
89 public final String role;
90 public RelationRole(Relation rel, String role) {
91 this.rel = rel;
92 this.role = role;
93 }
94
95 @Override
96 public int hashCode() {
97 return rel.hashCode();
98 }
99
100 @Override
101 public boolean equals(Object other) {
102 if (!(other instanceof RelationRole)) return false;
103 RelationRole otherMember = (RelationRole) other;
104 return otherMember.role.equals(role) && otherMember.rel.equals(rel);
105 }
106 }
107
108
109 /**
110 * HelperClass - saves a way and the "inside" side.
111 *
112 * insideToTheLeft: if true left side is "in", false -right side is "in".
113 * Left and right are determined along the orientation of way.
114 */
115 public static class WayInPolygon {
116 public final Way way;
117 public boolean insideToTheRight;
118
119 public WayInPolygon(Way _way, boolean _insideRight) {
120 this.way = _way;
121 this.insideToTheRight = _insideRight;
122 }
123
124 @Override
125 public int hashCode() {
126 return way.hashCode();
127 }
128
129 @Override
130 public boolean equals(Object other) {
131 if (!(other instanceof WayInPolygon)) return false;
132 WayInPolygon otherMember = (WayInPolygon) other;
133 return otherMember.way.equals(this.way) && otherMember.insideToTheRight == this.insideToTheRight;
134 }
135 }
136
137 /**
138 * This helper class describes a polygon, assembled from several ways.
139 * @author viesturs
140 *
141 */
142 public static class AssembledPolygon {
143 public List<WayInPolygon> ways;
144
145 public AssembledPolygon(List<WayInPolygon> boundary) {
146 this.ways = boundary;
147 }
148
149 public List<Node> getNodes() {
150 List<Node> nodes = new ArrayList<Node>();
151 for (WayInPolygon way : this.ways) {
152 //do not add the last node as it will be repeated in the next way
153 if (way.insideToTheRight) {
154 for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
155 nodes.add(way.way.getNode(pos));
156 }
157 }
158 else {
159 for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
160 nodes.add(way.way.getNode(pos));
161 }
162 }
163 }
164
165 return nodes;
166 }
167 }
168
169 public static class AssembledMultipolygon {
170 public AssembledPolygon outerWay;
171 public List<AssembledPolygon> innerWays;
172
173 public AssembledMultipolygon(AssembledPolygon way) {
174 outerWay = way;
175 innerWays = new ArrayList<AssembledPolygon>();
176 }
177 }
178
179 /**
180 * This hepler class implements algorithm traversing trough connected ways.
181 * Assumes you are going in clockwise orientation.
182 * @author viesturs
183 *
184 */
185 private static class WayTraverser {
186
187 private Set<WayInPolygon> availableWays;
188 private WayInPolygon lastWay;
189 private boolean lastWayReverse;
190
191 public WayTraverser(Collection<WayInPolygon> ways) {
192
193 availableWays = new HashSet<WayInPolygon>(ways);
194 lastWay = null;
195 }
196
197 public void removeWays(Collection<WayInPolygon> ways) {
198 availableWays.removeAll(ways);
199 }
200
201 public boolean hasWays() {
202 return availableWays.size() > 0;
203 }
204
205 public WayInPolygon startNewWay(WayInPolygon way) {
206 lastWay = way;
207 lastWayReverse = !lastWay.insideToTheRight;
208
209 return lastWay;
210 }
211
212 public WayInPolygon startNewWay() {
213 if (availableWays.isEmpty()) {
214 lastWay = null;
215 } else {
216 lastWay = availableWays.iterator().next();
217 lastWayReverse = !lastWay.insideToTheRight;
218 }
219
220 return lastWay;
221 }
222
223
224 public WayInPolygon advanceNextLeftmostWay() {
225 return advanceNextWay(false);
226 }
227
228 public WayInPolygon advanceNextRightmostWay() {
229 return advanceNextWay(true);
230 }
231
232 private WayInPolygon advanceNextWay(boolean rightmost) {
233
234 Node headNode = !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
235 Node prevNode = !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
236
237 //find best next way
238 WayInPolygon bestWay = null;
239 Node bestWayNextNode = null;
240 boolean bestWayReverse = false;
241
242 for (WayInPolygon way : availableWays) {
243 if (way.way.firstNode().equals(headNode)) {
244 //start adjacent to headNode
245 Node nextNode = way.way.getNode(1);
246
247 if (nextNode.equals(prevNode))
248 {
249 //this is the path we came from - ignore it.
250 }
251 else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
252 //the new way is better
253 bestWay = way;
254 bestWayReverse = false;
255 bestWayNextNode = nextNode;
256 }
257 }
258
259 if (way.way.lastNode().equals(headNode)) {
260 //end adjacent to headNode
261 Node nextNode = way.way.getNode(way.way.getNodesCount() - 2);
262
263 if (nextNode.equals(prevNode)) {
264 //this is the path we came from - ignore it.
265 }
266 else if (bestWay == null || (Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) {
267 //the new way is better
268 bestWay = way;
269 bestWayReverse = true;
270 bestWayNextNode = nextNode;
271 }
272 }
273 }
274
275 lastWay = bestWay;
276 lastWayReverse = bestWayReverse;
277
278 return lastWay;
279 }
280
281 public boolean isLastWayInsideToTheRight() {
282 return lastWayReverse != lastWay.insideToTheRight;
283 }
284
285 public Node getLastWayStartNode() {
286 return lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
287 }
288
289 public Node getLastWayEndNode() {
290 return lastWayReverse ? lastWay.way.firstNode() : lastWay.way.lastNode();
291 }
292 }
293
294 /**
295 * Helper storage class for finding findOuterWays
296 * @author viesturs
297 */
298 static class PolygonLevel {
299 public final int level;
300 public final AssembledMultipolygon pol;
301
302 public PolygonLevel(AssembledMultipolygon _pol, int _level) {
303 pol = _pol;
304 level = _level;
305 }
306 }
307
308 // Adds the menu entry, Shortcuts, etc.
309 public JoinAreasAction() {
310 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
311 Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
312 KeyEvent.VK_J, Shortcut.SHIFT), true);
313 }
314
315 /**
316 * Gets called whenever the shortcut is pressed or the menu entry is selected
317 * Checks whether the selected objects are suitable to join and joins them if so
318 */
319 public void actionPerformed(ActionEvent e) {
320 LinkedList<Way> ways = new LinkedList<Way>(Main.main.getCurrentDataSet().getSelectedWays());
321
322 if (ways.isEmpty()) {
323 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least one closed way that should be joined."));
324 return;
325 }
326
327 List<Node> allNodes = new ArrayList<Node>();
328 for (Way way : ways) {
329 if (!way.isClosed()) {
330 JOptionPane.showMessageDialog(Main.parent, tr("One of the selected ways is not closed and therefore cannot be joined."));
331 return;
332 }
333
334 allNodes.addAll(way.getNodes());
335 }
336
337 // TODO: Only display this warning when nodes outside dataSourceArea are deleted
338 Area dataSourceArea = Main.main.getCurrentDataSet().getDataSourceArea();
339 boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
340 trn("The selected way has nodes outside of the downloaded data region.",
341 "The selected ways have nodes outside of the downloaded data region.",
342 ways.size()) + "<br/>"
343 + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
344 + tr("Are you really sure to continue?")
345 + tr("Please abort if you are not sure"),
346 tr("The selected area is incomplete. Continue?"),
347 dataSourceArea, allNodes, null);
348 if(!ok) return;
349
350 //analyze multipolygon relations and collect all areas
351 List<Multipolygon> areas = collectMultipolygons(ways);
352
353 if (areas == null)
354 //too complex multipolygon relations found
355 return;
356
357 if (!testJoin(areas)) {
358 JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
359 return;
360 }
361
362 if (!resolveTagConflicts(areas))
363 return;
364 //user canceled, do nothing.
365
366 try {
367 JoinAreasResult result = joinAreas(areas);
368
369 if (result.hasChanges) {
370
371 List<Way> allWays = new ArrayList<Way>();
372 for (Multipolygon pol : result.polygons) {
373 allWays.add(pol.outerWay);
374 allWays.addAll(pol.innerWays);
375 }
376 DataSet ds = Main.main.getCurrentDataSet();
377 ds.setSelected(allWays);
378 Main.map.mapView.repaint();
379 } else {
380 JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
381 }
382 }
383 catch (UserCancelException exception) {
384 //revert changes
385 //FIXME: this is dirty hack
386 makeCommitsOneAction(tr("Reverting changes"));
387 Main.main.undoRedo.undo();
388 Main.main.undoRedo.redoCommands.clear();
389 }
390 }
391
392 /**
393 * Tests if the areas have some intersections to join.
394 * @param areas
395 * @return
396 */
397 private boolean testJoin(List<Multipolygon> areas) {
398 List<Way> allStartingWays = new ArrayList<Way>();
399
400 for (Multipolygon area : areas) {
401 allStartingWays.add(area.outerWay);
402 allStartingWays.addAll(area.innerWays);
403 }
404
405 //find intersection points
406 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
407 return nodes.size() > 0;
408 }
409
410 /**
411 * Will join two or more overlapping areas
412 * @param areas - list of areas to join
413 * @return new area formed.
414 */
415 private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
416
417 JoinAreasResult result = new JoinAreasResult();
418 result.hasChanges = false;
419
420 List<Way> allStartingWays = new ArrayList<Way>();
421 List<Way> innerStartingWays = new ArrayList<Way>();
422 List<Way> outerStartingWays = new ArrayList<Way>();
423
424 for (Multipolygon area : areas) {
425 outerStartingWays.add(area.outerWay);
426 innerStartingWays.addAll(area.innerWays);
427 }
428
429 allStartingWays.addAll(innerStartingWays);
430 allStartingWays.addAll(outerStartingWays);
431
432 //first remove nodes in the same coordinate
433 boolean removedDuplicates = false;
434 removedDuplicates |= removeDuplicateNodes(allStartingWays);
435
436 if (removedDuplicates) {
437 result.hasChanges = true;
438 commitCommands(marktr("Removed duplicate nodes"));
439 }
440
441 //find intersection points
442 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
443
444 //no intersections, return.
445 if (nodes.isEmpty())
446 return result;
447 commitCommands(marktr("Added node on all intersections"));
448
449 ArrayList<RelationRole> relations = new ArrayList<RelationRole>();
450
451 // Remove ways from all relations so ways can be combined/split quietly
452 for (Way way : allStartingWays) {
453 relations.addAll(removeFromAllRelations(way));
454 }
455
456 // Don't warn now, because it will really look corrupted
457 boolean warnAboutRelations = relations.size() > 0 && allStartingWays.size() > 1;
458
459 ArrayList<WayInPolygon> preparedWays = new ArrayList<WayInPolygon>();
460
461 for (Way way : outerStartingWays) {
462 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
463 preparedWays.addAll(markWayInsideSide(splitWays, false));
464 }
465
466 for (Way way : innerStartingWays) {
467 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
468 preparedWays.addAll(markWayInsideSide(splitWays, true));
469 }
470
471 // Find boundary ways
472 ArrayList<Way> discardedWays = new ArrayList<Way>();
473 List<AssembledPolygon> bounadries = findBoundaryPolygons(preparedWays, discardedWays);
474
475 //find polygons
476 List<AssembledMultipolygon> preparedPolygons = findPolygons(bounadries);
477
478
479 //assemble final polygons
480 List<Multipolygon> polygons = new ArrayList<Multipolygon>();
481 Set<Relation> relationsToDelete = new LinkedHashSet<Relation>();
482
483 for (AssembledMultipolygon pol : preparedPolygons) {
484
485 //create the new ways
486 Multipolygon resultPol = joinPolygon(pol);
487
488 //create multipolygon relation, if necessary.
489 RelationRole ownMultipolygonRelation = addOwnMultigonRelation(resultPol.innerWays, resultPol.outerWay);
490
491 //add back the original relations, merged with our new multipolygon relation
492 fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
493
494 //strip tags from inner ways
495 //TODO: preserve tags on existing inner ways
496 stripTags(resultPol.innerWays);
497
498 polygons.add(resultPol);
499 }
500
501 commitCommands(marktr("Assemble new polygons"));
502
503 for(Relation rel: relationsToDelete) {
504 cmds.add(new DeleteCommand(rel));
505 }
506
507 commitCommands(marktr("Delete relations"));
508
509 // Delete the discarded inner ways
510 if (discardedWays.size() > 0) {
511 cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), discardedWays, true));
512 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
513 }
514
515 makeCommitsOneAction(marktr("Joined overlapping areas"));
516
517 if (warnAboutRelations) {
518 JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced."));
519 }
520
521 result.hasChanges = true;
522 result.mergeSuccessful = true;
523 result.polygons = polygons;
524 return result;
525 }
526
527 /**
528 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
529 * @param Way First way to check
530 * @param Way Second Way to check
531 * @return boolean True if all conflicts are resolved, 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 way the way to process
559 * @return 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 String 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 == 1) {
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 witch ways are outer and witch are inner.
868 * @param boundaryWays
869 * @return
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 boundaryWays
889 * @return the outermostWay.
890 */
891 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
892
893 //TODO: bad performance for deep nestings...
894 List<PolygonLevel> result = new ArrayList<PolygonLevel>();
895
896 for (AssembledPolygon outerWay : boundaryWays) {
897
898 boolean outerGood = true;
899 List<AssembledPolygon> innerCandidates = new ArrayList<AssembledPolygon>();
900
901 for (AssembledPolygon innerWay : boundaryWays) {
902 if (innerWay == outerWay) {
903 continue;
904 }
905
906 if (wayInsideWay(outerWay, innerWay)) {
907 outerGood = false;
908 break;
909 } else if (wayInsideWay(innerWay, outerWay)) {
910 innerCandidates.add(innerWay);
911 }
912 }
913
914 if (!outerGood) {
915 continue;
916 }
917
918 //add new outer polygon
919 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
920 PolygonLevel polLev = new PolygonLevel(pol, level);
921
922 //process inner ways
923 if (innerCandidates.size() > 0) {
924 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
925 result.addAll(innerList);
926
927 for (PolygonLevel pl : innerList) {
928 if (pl.level == level + 1) {
929 pol.innerWays.add(pl.pol.outerWay);
930 }
931 }
932 }
933
934 result.add(polLev);
935 }
936
937 return result;
938 }
939
940 /**
941 * Finds all ways that form inner or outer boundaries.
942 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
943 * @param discardedResult this list is filled with ways that are to be discarded
944 * @return A list of ways that form the outer and inner boundaries of the multigon.
945 */
946 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, List<Way> discardedResult) {
947 //first find all discardable ways, by getting outer shells.
948 //this will produce incorrect boundaries in some cases, but second pass will fix it.
949
950 List<WayInPolygon> discardedWays = new ArrayList<WayInPolygon>();
951 Set<WayInPolygon> processedWays = new HashSet<WayInPolygon>();
952 WayTraverser traverser = new WayTraverser(multigonWays);
953
954 for (WayInPolygon startWay : multigonWays) {
955 if (processedWays.contains(startWay)) {
956 continue;
957 }
958
959 traverser.startNewWay(startWay);
960
961 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
962 WayInPolygon lastWay = startWay;
963
964 while (true) {
965 boundary.add(lastWay);
966
967 WayInPolygon bestWay = traverser.advanceNextLeftmostWay();
968 boolean wayInsideToTheRight = bestWay == null ? false : traverser.isLastWayInsideToTheRight();
969
970 if (bestWay == null || processedWays.contains(bestWay) || !wayInsideToTheRight) {
971 //bad segment chain - proceed to discard it
972 lastWay = null;
973 break;
974 } else if (boundary.contains(bestWay)) {
975 //traversed way found - close the way
976 lastWay = bestWay;
977 break;
978 } else {
979 //proceed to next segment
980 lastWay = bestWay;
981 }
982 }
983
984 if (lastWay != null) {
985 //way good
986 processedWays.addAll(boundary);
987
988 //remove junk segments at the start
989 while (boundary.get(0) != lastWay) {
990 discardedWays.add(boundary.get(0));
991 boundary.remove(0);
992 }
993 } else {
994 //way bad
995 discardedWays.addAll(boundary);
996 processedWays.addAll(boundary);
997 }
998 }
999
1000 //now we have removed junk segments, collect the real result ways
1001
1002 traverser.removeWays(discardedWays);
1003
1004 List<AssembledPolygon> result = new ArrayList<AssembledPolygon>();
1005
1006 while (traverser.hasWays()) {
1007
1008 WayInPolygon startWay = traverser.startNewWay();
1009 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1010 WayInPolygon curWay = startWay;
1011
1012 do {
1013 boundary.add(curWay);
1014 curWay = traverser.advanceNextRightmostWay();
1015
1016 //should not happen
1017 if (curWay == null || !traverser.isLastWayInsideToTheRight())
1018 throw new RuntimeException("Join areas internal error.");
1019
1020 } while (curWay != startWay);
1021
1022 //build result
1023 traverser.removeWays(boundary);
1024 result.add(new AssembledPolygon(boundary));
1025 }
1026
1027 for (WayInPolygon way : discardedWays) {
1028 discardedResult.add(way.way);
1029 }
1030
1031 //split inner polygons that have several touching parts.
1032 result = fixTouchingPolygons(result);
1033
1034 return result;
1035 }
1036
1037 /**
1038 * This method checks if polygons have several touching parts and splits them in several polygons.
1039 * @param polygons the polygons to process.
1040 */
1041 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons)
1042 {
1043 List<AssembledPolygon> newPolygons = new ArrayList<AssembledPolygon>();
1044
1045 for (AssembledPolygon innerPart : polygons) {
1046 WayTraverser traverser = new WayTraverser(innerPart.ways);
1047
1048 while (traverser.hasWays()) {
1049
1050 WayInPolygon startWay = traverser.startNewWay();
1051 List<WayInPolygon> boundary = new ArrayList<WayInPolygon>();
1052 WayInPolygon curWay = startWay;
1053
1054 Node startNode = traverser.getLastWayStartNode();
1055 boundary.add(curWay);
1056
1057 while (startNode != traverser.getLastWayEndNode()) {
1058 curWay = traverser.advanceNextLeftmostWay();
1059 boundary.add(curWay);
1060
1061 //should not happen
1062 if (curWay == null || !traverser.isLastWayInsideToTheRight())
1063 throw new RuntimeException("Join areas internal error.");
1064 }
1065
1066 //build result
1067 traverser.removeWays(boundary);
1068 newPolygons.add(new AssembledPolygon(boundary));
1069 }
1070 }
1071
1072 return newPolygons;
1073 }
1074
1075 /**
1076 * Tests if way is inside other way
1077 * @param outside
1078 * @param inside
1079 * @return
1080 */
1081 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1082 Set<Node> outsideNodes = new HashSet<Node>(outside.getNodes());
1083 List<Node> insideNodes = inside.getNodes();
1084
1085 for (Node insideNode : insideNodes) {
1086
1087 if (!outsideNodes.contains(insideNode))
1088 //simply test the one node
1089 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1090 }
1091
1092 //all nodes shared.
1093 return false;
1094 }
1095
1096 /**
1097 * Joins the lists of ways.
1098 * @param Collection<Way> The list of outer ways that belong to that multigon.
1099 * @return Way The newly created outer way
1100 */
1101 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1102 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1103
1104 for (AssembledPolygon pol : polygon.innerWays) {
1105 result.innerWays.add(joinWays(pol.ways));
1106 }
1107
1108 return result;
1109 }
1110
1111 /**
1112 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1113 * @param Collection<Way> The list of outer ways that belong to that multigon.
1114 * @return Way The newly created outer way
1115 */
1116 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1117
1118 //leave original orientation, if all paths are reverse.
1119 boolean allReverse = true;
1120 for (WayInPolygon way : ways) {
1121 allReverse &= !way.insideToTheRight;
1122 }
1123
1124 if (allReverse) {
1125 for (WayInPolygon way : ways) {
1126 way.insideToTheRight = !way.insideToTheRight;
1127 }
1128 }
1129
1130 Way joinedWay = joinOrientedWays(ways);
1131
1132 //should not happen
1133 if (joinedWay == null || !joinedWay.isClosed())
1134 throw new RuntimeException("Join areas internal error.");
1135
1136 return joinedWay;
1137 }
1138
1139 /**
1140 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1141 * @param ArrayList<Way> The list of ways to join and reverse
1142 * @return Way The newly created way
1143 */
1144 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException{
1145 if (ways.size() < 2)
1146 return ways.get(0).way;
1147
1148 // This will turn ways so all of them point in the same direction and CombineAction won't bug
1149 // the user about this.
1150
1151 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1152 List<Way> actionWays = new ArrayList<Way>(ways.size());
1153
1154 for (WayInPolygon way : ways) {
1155 actionWays.add(way.way);
1156
1157 if (!way.insideToTheRight) {
1158 ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1159 Main.main.undoRedo.add(res.getReverseCommand());
1160 cmdsCount++;
1161 }
1162 }
1163
1164 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1165
1166 Main.main.undoRedo.add(result.b);
1167 cmdsCount ++;
1168
1169 return result.a;
1170 }
1171
1172 /**
1173 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1174 * @param selectedWays the selected ways
1175 * @return list of polygons, or null if too complex relation encountered.
1176 */
1177 private List<Multipolygon> collectMultipolygons(List<Way> selectedWays) {
1178
1179 List<Multipolygon> result = new ArrayList<Multipolygon>();
1180
1181 //prepare the lists, to minimize memory allocation.
1182 List<Way> outerWays = new ArrayList<Way>();
1183 List<Way> innerWays = new ArrayList<Way>();
1184
1185 Set<Way> processedOuterWays = new LinkedHashSet<Way>();
1186 Set<Way> processedInnerWays = new LinkedHashSet<Way>();
1187
1188 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1189 if (r.isDeleted() || !r.isMultipolygon()) {
1190 continue;
1191 }
1192
1193 boolean hasKnownOuter = false;
1194 outerWays.clear();
1195 innerWays.clear();
1196
1197 for (RelationMember rm : r.getMembers()) {
1198 if (rm.getRole().equalsIgnoreCase("outer")) {
1199 outerWays.add(rm.getWay());
1200 hasKnownOuter |= selectedWays.contains(rm.getWay());
1201 }
1202 else if (rm.getRole().equalsIgnoreCase("inner")) {
1203 innerWays.add(rm.getWay());
1204 }
1205 }
1206
1207 if (!hasKnownOuter) {
1208 continue;
1209 }
1210
1211 if (outerWays.size() > 1) {
1212 JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."));
1213 return null;
1214 }
1215
1216 Way outerWay = outerWays.get(0);
1217
1218 //retain only selected inner ways
1219 innerWays.retainAll(selectedWays);
1220
1221 if (processedOuterWays.contains(outerWay)) {
1222 JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."));
1223 return null;
1224 }
1225
1226 if (processedInnerWays.contains(outerWay)) {
1227 JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."));
1228 return null;
1229 }
1230
1231 for (Way way :innerWays)
1232 {
1233 if (processedOuterWays.contains(way)) {
1234 JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."));
1235 return null;
1236 }
1237
1238 if (processedInnerWays.contains(way)) {
1239 JOptionPane.showMessageDialog(Main.parent, tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."));
1240 return null;
1241 }
1242 }
1243
1244 processedOuterWays.add(outerWay);
1245 processedInnerWays.addAll(innerWays);
1246
1247 Multipolygon pol = new Multipolygon(outerWay);
1248 pol.innerWays.addAll(innerWays);
1249 pol.relation = r;
1250
1251 result.add(pol);
1252 }
1253
1254 //add remaining ways, not in relations
1255 for (Way way : selectedWays) {
1256 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1257 continue;
1258 }
1259
1260 result.add(new Multipolygon(way));
1261 }
1262
1263 return result;
1264 }
1265
1266 /**
1267 * This method filters the list of relations that form the multipolygons.
1268 * @param relations
1269 * @param polygons
1270 * @return
1271 */
1272 private List<Relation> filterOwnMultipolygonRelations(Collection<Relation> relations, List<Multipolygon> polygons) {
1273
1274 List<Relation> relationsToRemove = new ArrayList<Relation>();
1275
1276 for (Multipolygon m : polygons) {
1277 if (m.relation != null) {
1278 relationsToRemove.add(m.relation);
1279 }
1280 }
1281
1282 List<Relation> result = new ArrayList<Relation>();
1283
1284 result.addAll(relations);
1285 result.removeAll(relationsToRemove);
1286 return result;
1287 }
1288
1289 /**
1290 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1291 * @param Collection<Way> List of already closed inner ways
1292 * @param Way The outer way
1293 * @param ArrayList<RelationRole> The list of relation with roles to add own relation to
1294 */
1295 private RelationRole addOwnMultigonRelation(Collection<Way> inner, Way outer) {
1296 if (inner.size() == 0) return null;
1297 // Create new multipolygon relation and add all inner ways to it
1298 Relation newRel = new Relation();
1299 newRel.put("type", "multipolygon");
1300 for (Way w : inner) {
1301 newRel.addMember(new RelationMember("inner", w));
1302 }
1303 cmds.add(new AddCommand(newRel));
1304
1305 // We don't add outer to the relation because it will be handed to fixRelations()
1306 // which will then do the remaining work.
1307 return new RelationRole(newRel, "outer");
1308 }
1309
1310 /**
1311 * Removes a given OsmPrimitive from all relations
1312 * @param OsmPrimitive Element to remove from all relations
1313 * @return ArrayList<RelationRole> List of relations with roles the primitives was part of
1314 */
1315 private ArrayList<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1316 ArrayList<RelationRole> result = new ArrayList<RelationRole>();
1317
1318 for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
1319 if (r.isDeleted()) {
1320 continue;
1321 }
1322 for (RelationMember rm : r.getMembers()) {
1323 if (rm.getMember() != osm) {
1324 continue;
1325 }
1326
1327 Relation newRel = new Relation(r);
1328 List<RelationMember> members = newRel.getMembers();
1329 members.remove(rm);
1330 newRel.setMembers(members);
1331
1332 cmds.add(new ChangeCommand(r, newRel));
1333 RelationRole saverel = new RelationRole(r, rm.getRole());
1334 if (!result.contains(saverel)) {
1335 result.add(saverel);
1336 }
1337 break;
1338 }
1339 }
1340
1341 commitCommands(marktr("Removed Element from Relations"));
1342 return result;
1343 }
1344
1345 /**
1346 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1347 * relations where the joined areas were in "outer" role a new relation is created instead with all
1348 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1349 * @param ArrayList<RelationRole> List of relations with roles the (original) ways were part of
1350 * @param Way The newly created outer area/way
1351 * @param relationsToDelete - set of relations to delete.
1352 */
1353 private void fixRelations(ArrayList<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1354 ArrayList<RelationRole> multiouters = new ArrayList<RelationRole>();
1355
1356 if (ownMultipol != null){
1357 multiouters.add(ownMultipol);
1358 }
1359
1360 for (RelationRole r : rels) {
1361 if (r.rel.isMultipolygon() && r.role.equalsIgnoreCase("outer")) {
1362 multiouters.add(r);
1363 continue;
1364 }
1365 // Add it back!
1366 Relation newRel = new Relation(r.rel);
1367 newRel.addMember(new RelationMember(r.role, outer));
1368 cmds.add(new ChangeCommand(r.rel, newRel));
1369 }
1370
1371 Relation newRel = null;
1372 switch (multiouters.size()) {
1373 case 0:
1374 return;
1375 case 1:
1376 // Found only one to be part of a multipolygon relation, so just add it back as well
1377 newRel = new Relation(multiouters.get(0).rel);
1378 newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1379 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1380 return;
1381 default:
1382 // Create a new relation with all previous members and (Way)outer as outer.
1383 newRel = new Relation();
1384 for (RelationRole r : multiouters) {
1385 // Add members
1386 for (RelationMember rm : r.rel.getMembers())
1387 if (!newRel.getMembers().contains(rm)) {
1388 newRel.addMember(rm);
1389 }
1390 // Add tags
1391 for (String key : r.rel.keySet()) {
1392 newRel.put(key, r.rel.get(key));
1393 }
1394 // Delete old relation
1395 relationsToDelete.add(r.rel);
1396 }
1397 newRel.addMember(new RelationMember("outer", outer));
1398 cmds.add(new AddCommand(newRel));
1399 }
1400 }
1401
1402 /**
1403 * @param Collection<Way> The List of Ways to remove all tags from
1404 */
1405 private void stripTags(Collection<Way> ways) {
1406 for (Way w : ways) {
1407 stripTags(w);
1408 }
1409 /* I18N: current action printed in status display */
1410 commitCommands(marktr("Remove tags from inner ways"));
1411 }
1412
1413 /**
1414 * @param Way The Way to remove all tags from
1415 */
1416 private void stripTags(Way x) {
1417 if (x.getKeys() == null)
1418 return;
1419 Way y = new Way(x);
1420 for (String key : x.keySet()) {
1421 y.remove(key);
1422 }
1423 cmds.add(new ChangeCommand(x, y));
1424 }
1425
1426 /**
1427 * Takes the last cmdsCount actions back and combines them into a single action
1428 * (for when the user wants to undo the join action)
1429 * @param String The commit message to display
1430 */
1431 private void makeCommitsOneAction(String message) {
1432 UndoRedoHandler ur = Main.main.undoRedo;
1433 cmds.clear();
1434 int i = Math.max(ur.commands.size() - cmdsCount, 0);
1435 for (; i < ur.commands.size(); i++) {
1436 cmds.add(ur.commands.get(i));
1437 }
1438
1439 for (i = 0; i < cmds.size(); i++) {
1440 ur.undo();
1441 }
1442
1443 commitCommands(message == null ? marktr("Join Areas Function") : message);
1444 cmdsCount = 0;
1445 }
1446
1447 @Override
1448 protected void updateEnabledState() {
1449 if (getCurrentDataSet() == null) {
1450 setEnabled(false);
1451 } else {
1452 updateEnabledState(getCurrentDataSet().getSelected());
1453 }
1454 }
1455
1456 @Override
1457 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1458 setEnabled(selection != null && !selection.isEmpty());
1459 }
1460}
Note: See TracBrowser for help on using the repository browser.