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

Last change on this file since 4067 was 4045, checked in by stoecker, 13 years ago

fix #6097 - i18n issues

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