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

Last change on this file since 10032 was 10001, checked in by Don-vip, 8 years ago

sonar - Local variable and method parameter names should comply with a naming convention

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