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

Last change on this file since 11635 was 11611, checked in by Don-vip, 7 years ago

fix #14123 - On first startup: Registered toolbar action overwritten

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