source: osm/applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/JoinAreasAction.java@ 16444

Last change on this file since 16444 was 16419, checked in by guggis, 16 years ago

updated for core r1758 after rework of projection handling and of editor layer access

File size: 34.7 KB
Line 
1package UtilsPlugin;
2
3import static org.openstreetmap.josm.tools.I18n.marktr;
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GridBagLayout;
7import java.awt.Polygon;
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.awt.geom.Line2D;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Map;
18import java.util.Set;
19import java.util.TreeMap;
20import java.util.TreeSet;
21import java.util.Map.Entry;
22
23import javax.swing.Box;
24import javax.swing.JComboBox;
25import javax.swing.JLabel;
26import javax.swing.JOptionPane;
27import javax.swing.JPanel;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.actions.CombineWayAction;
31import org.openstreetmap.josm.actions.JosmAction;
32import org.openstreetmap.josm.actions.ReverseWayAction;
33import org.openstreetmap.josm.actions.SplitWayAction;
34import org.openstreetmap.josm.command.AddCommand;
35import org.openstreetmap.josm.command.ChangeCommand;
36import org.openstreetmap.josm.command.Command;
37import org.openstreetmap.josm.command.DeleteCommand;
38import org.openstreetmap.josm.command.SequenceCommand;
39import org.openstreetmap.josm.data.Bounds;
40import org.openstreetmap.josm.data.UndoRedoHandler;
41import org.openstreetmap.josm.data.coor.EastNorth;
42import org.openstreetmap.josm.data.coor.LatLon;
43import org.openstreetmap.josm.data.osm.DataSet;
44import org.openstreetmap.josm.data.osm.DataSource;
45import org.openstreetmap.josm.data.osm.Node;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Relation;
48import org.openstreetmap.josm.data.osm.RelationMember;
49import org.openstreetmap.josm.data.osm.TigerUtils;
50import org.openstreetmap.josm.data.osm.Way;
51import org.openstreetmap.josm.gui.ExtendedDialog;
52import org.openstreetmap.josm.gui.layer.OsmDataLayer;
53import org.openstreetmap.josm.tools.GBC;
54import org.openstreetmap.josm.tools.Shortcut;
55
56public class JoinAreasAction extends JosmAction {
57 // This will be used to commit commands and unite them into one large command sequence at the end
58 private LinkedList<Command> cmds = new LinkedList<Command>();
59 private int cmdsCount = 0;
60
61 // HelperClass
62 // Saves a node and two positions where to insert the node into the ways
63 private class NodeToSegs implements Comparable<NodeToSegs> {
64 public int pos;
65 public Node n;
66 public double dis;
67 public NodeToSegs(int pos, Node n, LatLon dis) {
68 this.pos = pos;
69 this.n = n;
70 this.dis = n.getCoor().greatCircleDistance(dis);
71 }
72
73 public int compareTo(NodeToSegs o) {
74 if(this.pos == o.pos)
75 return (this.dis - o.dis) > 0 ? 1 : -1;
76 return this.pos - o.pos;
77 }
78 };
79
80 // HelperClass
81 // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
82 private class RelationRole {
83 public Relation rel;
84 public String role;
85 public RelationRole(Relation rel, String role) {
86 this.rel = rel;
87 this.role = role;
88 }
89
90 @Override
91 public boolean equals(Object other) {
92 if (!(other instanceof RelationRole)) return false;
93 RelationRole otherMember = (RelationRole) other;
94 return otherMember.role.equals(role) && otherMember.rel.equals(rel);
95 }
96 }
97
98 // Adds the menu entry, Shortcuts, etc.
99 public JoinAreasAction() {
100 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
101 KeyEvent.VK_J, Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), true);
102 }
103
104 /**
105 * Gets called whenever the shortcut is pressed or the menu entry is selected
106 * Checks whether the selected objects are suitable to join and joins them if so
107 */
108 public void actionPerformed(ActionEvent e) {
109 int result = new ExtendedDialog(Main.parent,
110 tr("Enter values for all conflicts."),
111 new JLabel(tr("THIS IS EXPERIMENTAL. Save your work and verify before uploading.")),
112 new String[] {tr("Continue anyway"), tr("Cancel")},
113 new String[] {"joinareas.png", "cancel.png"}).getValue();
114 if(result != 1) return;
115
116 Collection<OsmPrimitive> selection = Main.ds.getSelectedWays();
117
118 int ways = 0;
119 Way[] selWays = new Way[2];
120
121 LinkedList<Bounds> bounds = new LinkedList<Bounds>();
122 OsmDataLayer dataLayer = Main.map.mapView.getEditLayer();
123 for (DataSource ds : dataLayer.data.dataSources) {
124 if (ds.bounds != null)
125 bounds.add(ds.bounds);
126 }
127
128 boolean askedAlready = false;
129 for (OsmPrimitive prim : selection) {
130 Way way = (Way) prim;
131
132 // Too many ways
133 if(ways == 2) {
134 JOptionPane.showMessageDialog(Main.parent, tr("Only up to two areas can be joined at the moment."));
135 return;
136 }
137
138 if(!way.isClosed()) {
139 JOptionPane.showMessageDialog(Main.parent, tr("\"{0}\" is not closed and therefore can't be joined.", way.getName()));
140 return;
141 }
142
143 // This is copied from SimplifyAction and should be probably ported to tools
144 for (Node node : way.nodes) {
145 if(askedAlready) break;
146 boolean isInsideOneBoundingBox = false;
147 for (Bounds b : bounds) {
148 if (b.contains(node.getCoor())) {
149 isInsideOneBoundingBox = true;
150 break;
151 }
152 }
153
154 if (!isInsideOneBoundingBox) {
155 int option = JOptionPane.showConfirmDialog(Main.parent,
156 tr("The selected way(s) have nodes outside of the downloaded data region.\n"
157 + "This can lead to nodes being deleted accidentally.\n"
158 + "Are you really sure to continue?"),
159 tr("Please abort if you are not sure"), JOptionPane.YES_NO_OPTION,
160 JOptionPane.WARNING_MESSAGE);
161
162 if (option != JOptionPane.YES_OPTION) return;
163 askedAlready = true;
164 break;
165 }
166 }
167
168 selWays[ways] = way;
169 ways++;
170 }
171
172 if (ways < 1) {
173 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least one closed way the should be joined."));
174 return;
175 }
176
177 if(joinAreas(selWays[0], selWays[ways == 2 ? 1 : 0])) {
178 Main.map.mapView.repaint();
179 DataSet.fireSelectionChanged(Main.ds.getSelected());
180 } else
181 JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
182 }
183
184
185 /**
186 * Will join two overlapping areas
187 * @param Way First way/area
188 * @param Way Second way/area
189 * @return boolean Whether to display the "no operation" message
190 */
191 private boolean joinAreas(Way a, Way b) {
192 // Fix self-overlapping first or other errors
193 boolean same = a.equals(b);
194 boolean hadChanges = false;
195 if(!same) {
196 if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again
197 hadChanges = joinAreas(a, a);
198 hadChanges = joinAreas(b, b) || hadChanges;
199 }
200
201 ArrayList<OsmPrimitive> nodes = addIntersections(a, b);
202 if(nodes.size() == 0) return hadChanges;
203 commitCommands(marktr("Added node on all intersections"));
204
205 // Remove ways from all relations so ways can be combined/split quietly
206 ArrayList<RelationRole> relations = removeFromRelations(a);
207 if(!same) relations.addAll(removeFromRelations(b));
208
209 // Don't warn now, because it will really look corrupted
210 boolean warnAboutRelations = relations.size() > 0;
211
212 Collection<Way> allWays = splitWaysOnNodes(a, b, nodes);
213
214 // Find all nodes and inner ways save them to a list
215 Collection<Node> allNodes = getNodesFromWays(allWays);
216 Collection<Way> innerWays = findInnerWays(allWays, allNodes);
217
218 // Join outer ways
219 Way outerWay = joinOuterWays(allWays, innerWays);
220
221 // Fix Multipolygons if there are any
222 Collection<Way> newInnerWays = fixMultigons(innerWays, outerWay);
223
224 // Delete the remaining inner ways
225 if(innerWays != null && innerWays.size() > 0)
226 cmds.add(DeleteCommand.delete(innerWays, true));
227 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
228
229 // We can attach our new multipolygon relation and pretend it has always been there
230 addOwnMultigonRelation(newInnerWays, outerWay, relations);
231 fixRelations(relations, outerWay);
232 commitCommands(marktr("Fix relations"));
233
234 stripTags(newInnerWays);
235 makeCommitsOneAction(
236 same
237 ? marktr("Joined self-overlapping area")
238 : marktr("Joined overlapping areas")
239 );
240
241 if(warnAboutRelations)
242 JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced."));
243
244 return true;
245 }
246
247 /**
248 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
249 * @param Way First way to check
250 * @param Way Second Way to check
251 * @return boolean True if not all conflicts could be resolved, False if everything's fine
252 */
253 private boolean checkForTagConflicts(Way a, Way b) {
254 ArrayList<Way> ways = new ArrayList<Way>();
255 ways.add(a);
256 ways.add(b);
257
258 // This is mostly copied and pasted from CombineWayAction.java and one day should be moved into tools
259 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
260 for (Way w : ways) {
261 for (Entry<String,String> e : w.entrySet()) {
262 if (!props.containsKey(e.getKey()))
263 props.put(e.getKey(), new TreeSet<String>());
264 props.get(e.getKey()).add(e.getValue());
265 }
266 }
267
268 Way ax = new Way(a);
269 Way bx = new Way(b);
270
271 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
272 JPanel p = new JPanel(new GridBagLayout());
273 for (Entry<String, Set<String>> e : props.entrySet()) {
274 if (TigerUtils.isTigerTag(e.getKey())) {
275 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
276 ax.put(e.getKey(), combined);
277 bx.put(e.getKey(), combined);
278 } else if (e.getValue().size() > 1) {
279 if("created_by".equals(e.getKey()))
280 {
281 ax.put("created_by", "JOSM");
282 bx.put("created_by", "JOSM");
283 } else {
284 JComboBox c = new JComboBox(e.getValue().toArray());
285 c.setEditable(true);
286 p.add(new JLabel(e.getKey()), GBC.std());
287 p.add(Box.createHorizontalStrut(10), GBC.std());
288 p.add(c, GBC.eol());
289 components.put(e.getKey(), c);
290 }
291 } else {
292 String val = e.getValue().iterator().next();
293 ax.put(e.getKey(), val);
294 bx.put(e.getKey(), val);
295 }
296 }
297
298 if (components.isEmpty())
299 return false; // No conflicts found
300
301 int result = new ExtendedDialog(Main.parent,
302 tr("Enter values for all conflicts."),
303 p,
304 new String[] {tr("Solve Conflicts"), tr("Cancel")},
305 new String[] {"dialogs/conflict.png", "cancel.png"}).getValue();
306
307 if (result != 1) return true; // user cancel, unresolvable conflicts
308
309 for (Entry<String, JComboBox> e : components.entrySet()) {
310 String val = e.getValue().getEditor().getItem().toString();
311 ax.put(e.getKey(), val);
312 bx.put(e.getKey(), val);
313 }
314
315 cmds.add(new ChangeCommand(a, ax));
316 cmds.add(new ChangeCommand(b, bx));
317 commitCommands(marktr("Fix tag conflicts"));
318 return false;
319 }
320
321 /**
322 * Will find all intersection and add nodes there for two given ways
323 * @param Way First way
324 * @param Way Second way
325 * @return ArrayList<OsmPrimitive> List of new nodes
326 */
327 private ArrayList<OsmPrimitive> addIntersections(Way a, Way b) {
328 boolean same = a.equals(b);
329 int nodesSizeA = a.nodes.size();
330 int nodesSizeB = b.nodes.size();
331
332 // We use OsmPrimitive here instead of Node because we later need to split a way at these nodes.
333 // With OsmPrimitve we can simply add the way and don't have to loop over the nodes
334 ArrayList<OsmPrimitive> nodes = new ArrayList<OsmPrimitive>();
335 ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>();
336 ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>();
337
338 for (int i = (same ? 1 : 0); i < nodesSizeA - 1; i++) {
339 for (int j = (same ? i + 2 : 0); j < nodesSizeB - 1; j++) {
340 // Avoid re-adding nodes that already exist on (some) intersections
341 if(a.nodes.get(i).equals(b.nodes.get(j)) || a.nodes.get(i+1).equals(b.nodes.get(j))) {
342 nodes.add(b.nodes.get(j));
343 continue;
344 } else
345 if(a.nodes.get(i).equals(b.nodes.get(j+1)) || a.nodes.get(i+1).equals(b.nodes.get(j+1))) {
346 nodes.add(b.nodes.get(j+1));
347 continue;
348 }
349 LatLon intersection = getLineLineIntersection(
350 a.nodes.get(i) .getEastNorth().east(), a.nodes.get(i) .getEastNorth().north(),
351 a.nodes.get(i+1).getEastNorth().east(), a.nodes.get(i+1).getEastNorth().north(),
352 b.nodes.get(j) .getEastNorth().east(), b.nodes.get(j) .getEastNorth().north(),
353 b.nodes.get(j+1).getEastNorth().east(), b.nodes.get(j+1).getEastNorth().north());
354 if(intersection == null) continue;
355
356 // Create the node. Adding them to the ways must be delayed because we still loop over them
357 Node n = new Node(intersection);
358 cmds.add(new AddCommand(n));
359 nodes.add(n);
360 // The distance is needed to sort and add the nodes in direction of the way
361 nodesA.add(new NodeToSegs(i, n, a.nodes.get(i).getCoor()));
362 if(same)
363 nodesA.add(new NodeToSegs(j, n, a.nodes.get(j).getCoor()));
364 else
365 nodesB.add(new NodeToSegs(j, n, b.nodes.get(j).getCoor()));
366 }
367 }
368
369 addNodesToWay(a, nodesA);
370 if(!same) addNodesToWay(b, nodesB);
371
372 return nodes;
373 }
374
375 /**
376 * Finds the intersection of two lines
377 * @return LatLon null if no intersection was found, the LatLon coordinates of the intersection otherwise
378 */
379 static private LatLon getLineLineIntersection(
380 double x1, double y1, double x2, double y2,
381 double x3, double y3, double x4, double y4) {
382
383 if (!Line2D.linesIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return null;
384
385 // Convert line from (point, point) form to ax+by=c
386 double a1 = y2 - y1;
387 double b1 = x1 - x2;
388 double c1 = x2*y1 - x1*y2;
389
390 double a2 = y4 - y3;
391 double b2 = x3 - x4;
392 double c2 = x4*y3 - x3*y4;
393
394 // Solve the equations
395 double det = a1*b2 - a2*b1;
396 if(det == 0) return null; // Lines are parallel
397
398 return Main.proj.eastNorth2latlon(new EastNorth(
399 (b1*c2 - b2*c1)/det,
400 (a2*c1 -a1*c2)/det
401 ));
402 }
403
404 /**
405 * Inserts given nodes with positions into the given ways
406 * @param Way The way to insert the nodes into
407 * @param Collection<NodeToSegs> The list of nodes with positions to insert
408 */
409 private void addNodesToWay(Way a, ArrayList<NodeToSegs> nodes) {
410 Way ax=new Way(a);
411 Collections.sort(nodes);
412
413 int numOfAdds = 1;
414 for(NodeToSegs n : nodes) {
415 ax.addNode(n.pos + numOfAdds, n.n);
416 numOfAdds++;
417 }
418
419 cmds.add(new ChangeCommand(a, ax));
420 }
421
422 /**
423 * Commits the command list with a description
424 * @param String The description of what the commands do
425 */
426 private void commitCommands(String description) {
427 switch(cmds.size()) {
428 case 0:
429 return;
430 case 1:
431 Main.main.undoRedo.add(cmds.getFirst());
432 break;
433 default:
434 Command c = new SequenceCommand(tr(description), cmds);
435 Main.main.undoRedo.add(c);
436 break;
437 }
438
439 cmds.clear();
440 cmdsCount++;
441 }
442
443 /**
444 * Removes a given OsmPrimitive from all relations
445 * @param OsmPrimitive Element to remove from all relations
446 * @return ArrayList<RelationRole> List of relations with roles the primitives was part of
447 */
448 private ArrayList<RelationRole> removeFromRelations(OsmPrimitive osm) {
449 ArrayList<RelationRole> result = new ArrayList<RelationRole>();
450 for (Relation r : Main.ds.relations) {
451 if (r.deleted || r.incomplete) continue;
452 for (RelationMember rm : r.members) {
453 if (rm.member != osm) continue;
454
455 Relation newRel = new Relation(r);
456 newRel.members.remove(rm);
457
458 cmds.add(new ChangeCommand(r, newRel));
459 RelationRole saverel = new RelationRole(r, rm.role);
460 if(!result.contains(saverel)) result.add(saverel);
461 break;
462 }
463 }
464
465 commitCommands(marktr("Removed Element from Relations"));
466 return result;
467 }
468
469 /**
470 * This is a hacky implementation to make use of the splitWayAction code and
471 * should be improved. SplitWayAction needs to expose its splitWay function though.
472 */
473 private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) {
474 ArrayList<Way> ways = new ArrayList<Way>();
475 ways.add(a);
476 if(!a.equals(b)) ways.add(b);
477
478 List<OsmPrimitive> affected = new ArrayList<OsmPrimitive>();
479 for (Way way : ways) {
480 nodes.add(way);
481 Main.ds.setSelected(nodes);
482 nodes.remove(way);
483 new SplitWayAction().actionPerformed(null);
484 cmdsCount++;
485 affected.addAll(Main.ds.getSelectedWays());
486 }
487 return osmprim2way(affected);
488 }
489
490 /**
491 * Converts a list of OsmPrimitives to a list of Ways
492 * @param Collection<OsmPrimitive> The OsmPrimitives list that's needed as a list of Ways
493 * @return Collection<Way> The list as list of Ways
494 */
495 static private Collection<Way> osmprim2way(Collection<OsmPrimitive> ways) {
496 Collection<Way> result = new ArrayList<Way>();
497 for(OsmPrimitive w: ways) {
498 if(w instanceof Way) result.add((Way) w);
499 }
500 return result;
501 }
502
503 /**
504 * Returns all nodes for given ways
505 * @param Collection<Way> The list of ways which nodes are to be returned
506 * @return Collection<Node> The list of nodes the ways contain
507 */
508 private Collection<Node> getNodesFromWays(Collection<Way> ways) {
509 Collection<Node> allNodes = new ArrayList<Node>();
510 for(Way w: ways) allNodes.addAll(w.nodes);
511 return allNodes;
512 }
513
514 /**
515 * Finds all inner ways for a given list of Ways and Nodes from a multigon by constructing a polygon
516 * for each way, looking for inner nodes that are not part of this way. If a node is found, all ways
517 * containing this node are added to the list
518 * @param Collection<Way> A list of (splitted) ways that form a multigon
519 * @param Collection<Node> A list of nodes that belong to the multigon
520 * @return Collection<Way> A list of ways that are positioned inside the outer borders of the multigon
521 */
522 private Collection<Way> findInnerWays(Collection<Way> multigonWays, Collection<Node> multigonNodes) {
523 Collection<Way> innerWays = new ArrayList<Way>();
524 for(Way w: multigonWays) {
525 Polygon poly = new Polygon();
526 for(Node n: (w).nodes) poly.addPoint(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon()));
527
528 for(Node n: multigonNodes) {
529 if(!(w).nodes.contains(n) && poly.contains(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon()))) {
530 getWaysByNode(innerWays, multigonWays, n);
531 }
532 }
533 }
534
535 return innerWays;
536 }
537
538 // Polygon only supports int coordinates, so convert them
539 private int latlonToXY(double val) {
540 return (int)Math.round(val*1000000);
541 }
542
543 /**
544 * Finds all ways that contain the given node.
545 * @param Collection<Way> A list to which matching ways will be added
546 * @param Collection<Way> A list of ways to check
547 * @param Node The node the ways should be checked against
548 */
549 private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) {
550 for(Way way : w) {
551 if(!(way).nodes.contains(n)) continue;
552 if(!innerWays.contains(way)) innerWays.add(way); // Will need this later for multigons
553 }
554 }
555
556 /**
557 * Joins the two outer ways and deletes all short ways that can't be part of a multipolygon anyway
558 * @param Collection<OsmPrimitive> The list of all ways that belong to that multigon
559 * @param Collection<Way> The list of inner ways that belong to that multigon
560 * @return Way The newly created outer way
561 */
562 private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) {
563 ArrayList<Way> join = new ArrayList<Way>();
564 for(Way w: multigonWays) {
565 // Skip inner ways
566 if(innerWays.contains(w)) continue;
567
568 if(w.nodes.size() <= 2)
569 cmds.add(new DeleteCommand(w));
570 else
571 join.add(w);
572 }
573
574 commitCommands(marktr("Join Areas: Remove Short Ways"));
575 return closeWay(joinWays(join));
576 }
577
578 /**
579 * Ensures a way is closed. If it isn't, last and first node are connected.
580 * @param Way the way to ensure it's closed
581 * @return Way The joined way.
582 */
583 private Way closeWay(Way w) {
584 if(w.isClosed())
585 return w;
586 Main.ds.setSelected(w);
587 Way wnew = new Way(w);
588 wnew.addNode(wnew.firstNode());
589 cmds.add(new ChangeCommand(w, wnew));
590 commitCommands(marktr("Closed Way"));
591 return (Way)(Main.ds.getSelectedWays().toArray())[0];
592 }
593
594 /**
595 * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former)
596 * @param ArrayList<Way> The list of ways to join
597 * @return Way The newly created way
598 */
599 private Way joinWays(ArrayList<Way> ways) {
600 if(ways.size() < 2) return ways.get(0);
601
602 // This will turn ways so all of them point in the same direction and CombineAction won't bug
603 // the user about this.
604 Way a = null;
605 for(Way b : ways) {
606 if(a == null) {
607 a = b;
608 continue;
609 }
610 if(a.nodes.get(0).equals(b.nodes.get(0)) ||
611 a.nodes.get(a.nodes.size()-1).equals(b.nodes.get(b.nodes.size()-1))) {
612 Main.ds.setSelected(b);
613 new ReverseWayAction().actionPerformed(null);
614 cmdsCount++;
615 }
616 a = b;
617 }
618 Main.ds.setSelected(ways);
619 // TODO: It might be possible that a confirmation dialog is presented even after reversing (for
620 // "strange" ways). If the user cancels this, makeCommitsOneAction will wrongly consume a previous
621 // action. Make CombineWayAction either silent or expose its combining capabilities.
622 new CombineWayAction().actionPerformed(null);
623 cmdsCount++;
624 return (Way)(Main.ds.getSelectedWays().toArray())[0];
625 }
626
627 /**
628 * Finds all ways that may be part of a multipolygon relation and removes them from the given list.
629 * It will automatically combine "good" ways
630 * @param Collection<Way> The list of inner ways to check
631 * @param Way The newly created outer way
632 * @return ArrayList<Way> The List of newly created inner ways
633 */
634 private ArrayList<Way> fixMultigons(Collection<Way> uninterestingWays, Way outerWay) {
635 Collection<Node> innerNodes = getNodesFromWays(uninterestingWays);
636 Collection<Node> outerNodes = outerWay.nodes;
637
638 // The newly created inner ways. uninterestingWays is passed by reference and therefore modified in-place
639 ArrayList<Way> newInnerWays = new ArrayList<Way>();
640
641 // Now we need to find all inner ways that contain a remaining node, but no outer nodes
642 // Remaining nodes are those that contain to more than one way. All nodes that belong to an
643 // inner multigon part will have at least two ways, so we can use this to find which ways do
644 // belong to the multigon.
645 ArrayList<Way> possibleWays = new ArrayList<Way>();
646 wayIterator: for(Way w : uninterestingWays) {
647 boolean hasInnerNodes = false;
648 for(Node n : w.nodes) {
649 if(outerNodes.contains(n)) continue wayIterator;
650 if(!hasInnerNodes && innerNodes.contains(n)) hasInnerNodes = true;
651 }
652 if(!hasInnerNodes || w.nodes.size() < 2) continue;
653 possibleWays.add(w);
654 }
655
656 // This removes unnecessary ways that might have been added.
657 removeAlmostAlikeWays(possibleWays);
658 removePartlyUnconnectedWays(possibleWays);
659
660 // Join all ways that have one start/ending node in common
661 Way joined = null;
662 outerIterator: do {
663 joined = null;
664 for(Way w1 : possibleWays) {
665 if(w1.isClosed()) {
666 if(!wayIsCollapsed(w1)) {
667 uninterestingWays.remove(w1);
668 newInnerWays.add(w1);
669 }
670 joined = w1;
671 possibleWays.remove(w1);
672 continue outerIterator;
673 }
674 for(Way w2 : possibleWays) {
675 // w2 cannot be closed, otherwise it would have been removed above
676 if(!waysCanBeCombined(w1, w2)) continue;
677
678 ArrayList<Way> joinThem = new ArrayList<Way>();
679 joinThem.add(w1);
680 joinThem.add(w2);
681 uninterestingWays.removeAll(joinThem);
682 possibleWays.removeAll(joinThem);
683
684 // Although we joined the ways, we cannot simply assume that they are closed
685 joined = joinWays(joinThem);
686 uninterestingWays.add(joined);
687 possibleWays.add(joined);
688 continue outerIterator;
689 }
690 }
691 } while(joined != null);
692 return newInnerWays;
693 }
694
695 /**
696 * Removes almost alike ways (= ways that are on top of each other for all nodes)
697 * @param ArrayList<Way> the ways to remove almost-duplicates from
698 */
699 private void removeAlmostAlikeWays(ArrayList<Way> ways) {
700 Collection<Way> removables = new ArrayList<Way>();
701 outer: for(int i=0; i < ways.size(); i++) {
702 Way a = ways.get(i);
703 for(int j=i+1; j < ways.size(); j++) {
704 Way b = ways.get(j);
705 List<Node> revNodes = new ArrayList<Node>(b.nodes);
706 Collections.reverse(revNodes);
707 if(a.nodes.equals(b.nodes) || a.nodes.equals(revNodes)) {
708 removables.add(a);
709 continue outer;
710 }
711 }
712 }
713 ways.removeAll(removables);
714 }
715
716 /**
717 * Removes ways from the given list whose starting or ending node doesn't
718 * connect to other ways from the same list (it's like removing spikes).
719 * @param ArrayList<Way> The list of ways to remove "spikes" from
720 */
721 private void removePartlyUnconnectedWays(ArrayList<Way> ways) {
722 List<Way> removables = new ArrayList<Way>();
723 for(Way a : ways) {
724 if(a.isClosed()) continue;
725 boolean connectedStart = false;
726 boolean connectedEnd = false;
727 for(Way b : ways) {
728 if(a.equals(b))
729 continue;
730 if(b.isFirstLastNode(a.firstNode()))
731 connectedStart = true;
732 if(b.isFirstLastNode(a.lastNode()))
733 connectedEnd = true;
734 }
735 if(!connectedStart || !connectedEnd)
736 removables.add(a);
737 }
738 ways.removeAll(removables);
739 }
740
741 /**
742 * Checks if a way is collapsed (i.e. looks like <---->)
743 * @param Way A *closed* way to check if it is collapsed
744 * @return boolean If the closed way is collapsed or not
745 */
746 private boolean wayIsCollapsed(Way w) {
747 if(w.nodes.size() <= 3) return true;
748
749 // If a way contains more than one node twice, it must be collapsed (only start/end node may be the same)
750 Way x = new Way(w);
751 int count = 0;
752 for(Node n : w.nodes) {
753 x.nodes.remove(n);
754 if(x.nodes.contains(n)) count++;
755 if(count == 2) return true;
756 }
757 return false;
758 }
759
760 /**
761 * Checks if two ways share one starting/ending node
762 * @param Way first way
763 * @param Way second way
764 * @return boolean Wheter the ways share a starting/ending node or not
765 */
766 private boolean waysCanBeCombined(Way w1, Way w2) {
767 if(w1.equals(w2)) return false;
768
769 if(w1.nodes.get(0).equals(w2.nodes.get(0))) return true;
770 if(w1.nodes.get(0).equals(w2.nodes.get(w2.nodes.size()-1))) return true;
771
772 if(w1.nodes.get(w1.nodes.size()-1).equals(w2.nodes.get(0))) return true;
773 if(w1.nodes.get(w1.nodes.size()-1).equals(w2.nodes.get(w2.nodes.size()-1))) return true;
774
775 return false;
776 }
777
778 /**
779 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
780 * @param Collection<Way> List of already closed inner ways
781 * @param Way The outer way
782 * @param ArrayList<RelationRole> The list of relation with roles to add own relation to
783 */
784 private void addOwnMultigonRelation(Collection<Way> inner, Way outer, ArrayList<RelationRole> rels) {
785 if(inner.size() == 0) return;
786 // Create new multipolygon relation and add all inner ways to it
787 Relation newRel = new Relation();
788 newRel.put("type", "multipolygon");
789 for(Way w : inner)
790 newRel.members.add(new RelationMember("inner", w));
791 cmds.add(new AddCommand(newRel));
792
793 // We don't add outer to the relation because it will be handed to fixRelations()
794 // which will then do the remaining work. Collections are passed by reference, so no
795 // need to return it
796 rels.add(new RelationRole(newRel, "outer"));
797 //return rels;
798 }
799
800 /**
801 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
802 * relations where the joined areas were in "outer" role a new relation is created instead with all
803 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
804 * @param ArrayList<RelationRole> List of relations with roles the (original) ways were part of
805 * @param Way The newly created outer area/way
806 */
807 private void fixRelations(ArrayList<RelationRole> rels, Way outer) {
808 ArrayList<RelationRole> multiouters = new ArrayList<RelationRole>();
809 for(RelationRole r : rels) {
810 if( r.rel.get("type") != null &&
811 r.rel.get("type").equalsIgnoreCase("multipolygon") &&
812 r.role.equalsIgnoreCase("outer")
813 ) {
814 multiouters.add(r);
815 continue;
816 }
817 // Add it back!
818 Relation newRel = new Relation(r.rel);
819 newRel.members.add(new RelationMember(r.role, outer));
820 cmds.add(new ChangeCommand(r.rel, newRel));
821 }
822
823 Relation newRel = null;
824 switch(multiouters.size()) {
825 case 0:
826 return;
827 case 1:
828 // Found only one to be part of a multipolygon relation, so just add it back as well
829 newRel = new Relation(multiouters.get(0).rel);
830 newRel.members.add(new RelationMember(multiouters.get(0).role, outer));
831 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
832 return;
833 default:
834 // Create a new relation with all previous members and (Way)outer as outer.
835 newRel = new Relation();
836 for(RelationRole r : multiouters) {
837 // Add members
838 for(RelationMember rm : r.rel.members)
839 if(!newRel.members.contains(rm)) newRel.members.add(rm);
840 // Add tags
841 for (String key : r.rel.keys.keySet()) {
842 newRel.put(key, r.rel.keys.get(key));
843 }
844 // Delete old relation
845 cmds.add(new DeleteCommand(r.rel));
846 }
847 newRel.members.add(new RelationMember("outer", outer));
848 cmds.add(new AddCommand(newRel));
849 }
850 }
851
852 /**
853 * @param Collection<Way> The List of Ways to remove all tags from
854 */
855 private void stripTags(Collection<Way> ways) {
856 for(Way w: ways) stripTags(w);
857 commitCommands(marktr("Remove tags from inner ways"));
858 }
859
860 /**
861 * @param Way The Way to remove all tags from
862 */
863 private void stripTags(Way x) {
864 if(x.keys == null) return;
865 Way y = new Way(x);
866 for (String key : x.keys.keySet())
867 y.remove(key);
868 cmds.add(new ChangeCommand(x, y));
869 }
870
871 /**
872 * Takes the last cmdsCount actions back and combines them into a single action
873 * (for when the user wants to undo the join action)
874 * @param String The commit message to display
875 */
876 private void makeCommitsOneAction(String message) {
877 UndoRedoHandler ur = Main.main.undoRedo;
878 cmds.clear();
879 int i = Math.max(ur.commands.size() - cmdsCount, 0);
880 for(; i < ur.commands.size(); i++)
881 cmds.add(ur.commands.get(i));
882
883 for(i = 0; i < cmds.size(); i++)
884 ur.undo();
885
886 commitCommands(message == null ? marktr("Join Areas Function") : message);
887 cmdsCount = 0;
888 }
889}
Note: See TracBrowser for help on using the repository browser.