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

Last change on this file since 3445 was 3445, checked in by bastiK, 14 years ago

see #5179 (patch by viesturs) - Join overlapping areas produces bad results

  • Property svn:eol-style set to native
File size: 46.3 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
10import java.awt.event.KeyEvent;
11import java.awt.geom.Area;
12import java.awt.geom.Line2D;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22import java.util.TreeMap;
23import java.util.TreeSet;
24import java.util.Map.Entry;
25
26import javax.swing.Box;
27import javax.swing.JComboBox;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31
32import org.openstreetmap.josm.Main;
33import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
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.UndoRedoHandler;
40import org.openstreetmap.josm.data.coor.EastNorth;
41import org.openstreetmap.josm.data.coor.LatLon;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.Node;
44import org.openstreetmap.josm.data.osm.OsmPrimitive;
45import org.openstreetmap.josm.data.osm.Relation;
46import org.openstreetmap.josm.data.osm.RelationMember;
47import org.openstreetmap.josm.data.osm.TigerUtils;
48import org.openstreetmap.josm.data.osm.Way;
49import org.openstreetmap.josm.gui.ExtendedDialog;
50import org.openstreetmap.josm.tools.GBC;
51import org.openstreetmap.josm.tools.Shortcut;
52
53public class JoinAreasAction extends JosmAction {
54 // This will be used to commit commands and unite them into one large command sequence at the end
55 private LinkedList<Command> cmds = new LinkedList<Command>();
56 private int cmdsCount = 0;
57
58 /**
59 * This helper class describes join ares action result.
60 * @author viesturs
61 *
62 */
63 public static class JoinAreasResult {
64
65 public Way outerWay;
66 public List<Way> innerWays;
67
68 public boolean mergeSuccessful;
69 public boolean hasChanges;
70 public boolean hasRelationProblems;
71 }
72
73 // HelperClass
74 // Saves a node and two positions where to insert the node into the ways
75 private static class NodeToSegs implements Comparable<NodeToSegs> {
76 public int pos;
77 public Node n;
78 public double dis;
79 public NodeToSegs(int pos, Node n, LatLon dis) {
80 this.pos = pos;
81 this.n = n;
82 this.dis = n.getCoor().greatCircleDistance(dis);
83 }
84
85 public int compareTo(NodeToSegs o) {
86 if(this.pos == o.pos)
87 return (this.dis - o.dis) > 0 ? 1 : -1;
88 return this.pos - o.pos;
89 }
90
91 @Override
92 public int hashCode() {
93 return pos;
94 }
95
96 @Override
97 public boolean equals(Object o) {
98 if (o instanceof NodeToSegs)
99 return compareTo((NodeToSegs) o) == 0;
100 else
101 return false;
102 }
103 }
104
105 // HelperClass
106 // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
107 private static class RelationRole {
108 public final Relation rel;
109 public final String role;
110 public RelationRole(Relation rel, String role) {
111 this.rel = rel;
112 this.role = role;
113 }
114
115 @Override
116 public int hashCode() {
117 return rel.hashCode();
118 }
119
120 @Override
121 public boolean equals(Object other) {
122 if (!(other instanceof RelationRole)) return false;
123 RelationRole otherMember = (RelationRole) other;
124 return otherMember.role.equals(role) && otherMember.rel.equals(rel);
125 }
126 }
127
128 /**
129 * HelperClass
130 * saves a way and the "inside" side
131 * insideToTheLeft: if true left side is "in", false -right side is "in".
132 * Left and right are determined along the orientation of way.
133 */
134 private static class WayInPath {
135 public final Way way;
136 public boolean insideToTheLeft;
137
138 public WayInPath(Way way, boolean insideLeft) {
139 this.way = way;
140 this.insideToTheLeft = insideLeft;
141 }
142
143 @Override
144 public int hashCode() {
145 return way.hashCode();
146 }
147
148 @Override
149 public boolean equals(Object other) {
150 if (!(other instanceof WayInPath))
151 return false;
152 WayInPath otherMember = (WayInPath) other;
153 return otherMember.way.equals(this.way) && otherMember.insideToTheLeft == this.insideToTheLeft;
154 }
155 }
156
157 // Adds the menu entry, Shortcuts, etc.
158 public JoinAreasAction() {
159 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
160 KeyEvent.VK_J, Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), true);
161 }
162
163 /**
164 * Gets called whenever the shortcut is pressed or the menu entry is selected
165 * Checks whether the selected objects are suitable to join and joins them if so
166 */
167 public void actionPerformed(ActionEvent e) {
168 LinkedList<Way> ways = new LinkedList<Way>(Main.main.getCurrentDataSet().getSelectedWays());
169
170 if (ways.isEmpty()) {
171 JOptionPane.showMessageDialog(Main.parent, tr("Please select at least one closed way that should be joined."));
172 return;
173 }
174
175 // Too many ways
176 if(ways.size() > 2) {
177 JOptionPane.showMessageDialog(Main.parent, tr("Only up to two areas can be joined at the moment."));
178 return;
179 }
180
181 List<Node> allNodes = new ArrayList<Node>();
182 for (Way way: ways) {
183 if(!way.isClosed()) {
184 JOptionPane.showMessageDialog(Main.parent, tr("\"{0}\" is not closed and therefore cannot be joined.", way.getName()));
185 return;
186 }
187
188 allNodes.addAll(way.getNodes());
189 }
190
191 // TODO: Only display this warning when nodes outside dataSourceArea are deleted
192 Area dataSourceArea = Main.main.getCurrentDataSet().getDataSourceArea();
193 if (dataSourceArea != null) {
194 for (Node node: allNodes) {
195 if (!dataSourceArea.contains(node.getCoor())) {
196 int option = JOptionPane.showConfirmDialog(Main.parent,
197 trn("The selected way has nodes outside of the downloaded data region.",
198 "The selected ways have nodes outside of the downloaded data region.",
199 ways.size()) + "\n"
200 + tr("This can lead to nodes being deleted accidentally.") + "\n"
201 + tr("Are you really sure to continue?"),
202 tr("Please abort if you are not sure"), JOptionPane.YES_NO_OPTION,
203 JOptionPane.WARNING_MESSAGE);
204
205 if (option != JOptionPane.YES_OPTION) return;
206 break;
207 }
208 }
209 }
210
211 if (checkForTagConflicts(ways.getFirst(), ways.getLast())) {
212 //do nothing. //FIXME: abort?
213 }
214
215 JoinAreasResult result = joinAreas(ways.getFirst(), ways.getLast());
216
217 if (result.hasChanges) {
218 Main.map.mapView.repaint();
219 DataSet ds = Main.main.getCurrentDataSet();
220 ds.fireSelectionChanged();
221 } else {
222 JOptionPane.showMessageDialog(Main.parent, tr("No intersection found. Nothing was changed."));
223 }
224 }
225
226 /**
227 * Will join two overlapping areas
228 * @param Way First way/area
229 * @param Way Second way/area
230 */
231 private JoinAreasResult joinAreas(Way a, Way b) {
232
233 JoinAreasResult result = new JoinAreasResult();
234 result.hasChanges = false;
235
236 // Fix self-overlapping first or other errors
237 boolean same = a.equals(b);
238 if(!same) {
239 int i = 0;
240
241 //join each area with itself, fixing self-crossings.
242 JoinAreasResult resultA = joinAreas(a, a);
243 JoinAreasResult resultB = joinAreas(b, b);
244
245 if (resultA.mergeSuccessful) {
246 a = resultA.outerWay;
247 ++i;
248 }
249 if(resultB.mergeSuccessful) {
250 b = resultB.outerWay;
251 ++i;
252 }
253
254 result.hasChanges = i > 0;
255 cmdsCount = i;
256 }
257
258 ArrayList<Node> nodes = addIntersections(a, b);
259
260 //no intersections, return.
261 if(nodes.size() == 0) return result;
262 commitCommands(marktr("Added node on all intersections"));
263
264 // Remove ways from all relations so ways can be combined/split quietly
265 ArrayList<RelationRole> relations = removeFromRelations(a);
266 if(!same) {
267 relations.addAll(removeFromRelations(b));
268 }
269
270 // Don't warn now, because it will really look corrupted
271 boolean warnAboutRelations = relations.size() > 0;
272
273 ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes);
274
275 // Find inner ways save them to a list
276 ArrayList<WayInPath> outerWays = findOuterWays(allWays);
277 ArrayList<Way> innerWays = findInnerWays(allWays, outerWays);
278
279 // Join outer ways
280 Way outerWay = joinOuterWays(outerWays);
281
282 // Fix Multipolygons if there are any
283 List<Way> newInnerWays = fixMultipolygons(innerWays, outerWay, same);
284
285 // Delete the remaining inner ways
286 if(innerWays != null && innerWays.size() > 0) {
287 cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), innerWays, true));
288 }
289 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
290
291 // We can attach our new multipolygon relation and pretend it has always been there
292 addOwnMultigonRelation(newInnerWays, outerWay, relations);
293 fixRelations(relations, outerWay);
294 commitCommands(marktr("Fix relations"));
295
296 stripTags(newInnerWays);
297
298 makeCommitsOneAction(
299 same
300 ? marktr("Joined self-overlapping area")
301 : marktr("Joined overlapping areas")
302 );
303
304 if(warnAboutRelations) {
305 JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced."));
306 }
307
308 result.mergeSuccessful = true;
309 result.outerWay = outerWay;
310 result.innerWays = newInnerWays;
311
312 return result;
313 }
314
315 /**
316 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
317 * @param Way First way to check
318 * @param Way Second Way to check
319 * @return boolean True if not all conflicts could be resolved, False if everything's fine
320 */
321 private boolean checkForTagConflicts(Way a, Way b) {
322 ArrayList<Way> ways = new ArrayList<Way>();
323 ways.add(a);
324 ways.add(b);
325
326 // FIXME: This is mostly copied and pasted from CombineWayAction.java and one day should be moved into tools
327 // We have TagCollection handling for that now - use it here as well
328 Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
329 for (Way w : ways) {
330 for (String key: w.keySet()) {
331 if (!props.containsKey(key)) {
332 props.put(key, new TreeSet<String>());
333 }
334 props.get(key).add(w.get(key));
335 }
336 }
337
338 Way ax = new Way(a);
339 Way bx = new Way(b);
340
341 Map<String, JComboBox> components = new HashMap<String, JComboBox>();
342 JPanel p = new JPanel(new GridBagLayout());
343 for (Entry<String, Set<String>> e : props.entrySet()) {
344 if (TigerUtils.isTigerTag(e.getKey())) {
345 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
346 ax.put(e.getKey(), combined);
347 bx.put(e.getKey(), combined);
348 } else if (e.getValue().size() > 1) {
349 if("created_by".equals(e.getKey()))
350 {
351 ax.remove("created_by");
352 bx.remove("created_by");
353 } else {
354 JComboBox c = new JComboBox(e.getValue().toArray());
355 c.setEditable(true);
356 p.add(new JLabel(e.getKey()), GBC.std());
357 p.add(Box.createHorizontalStrut(10), GBC.std());
358 p.add(c, GBC.eol());
359 components.put(e.getKey(), c);
360 }
361 } else {
362 String val = e.getValue().iterator().next();
363 ax.put(e.getKey(), val);
364 bx.put(e.getKey(), val);
365 }
366 }
367
368 if (components.isEmpty())
369 return false; // No conflicts found
370
371 ExtendedDialog ed = new ExtendedDialog(Main.parent,
372 tr("Enter values for all conflicts."),
373 new String[] {tr("Solve Conflicts"), tr("Cancel")});
374 ed.setButtonIcons(new String[] {"dialogs/conflict.png", "cancel.png"});
375 ed.setContent(p);
376 ed.showDialog();
377
378 if (ed.getValue() != 1) return true; // user cancel, unresolvable conflicts
379
380 for (Entry<String, JComboBox> e : components.entrySet()) {
381 String val = e.getValue().getEditor().getItem().toString();
382 ax.put(e.getKey(), val);
383 bx.put(e.getKey(), val);
384 }
385
386 cmds.add(new ChangeCommand(a, ax));
387 cmds.add(new ChangeCommand(b, bx));
388 commitCommands(marktr("Fix tag conflicts"));
389 return false;
390 }
391
392 /**
393 * Will find all intersection and add nodes there for two given ways
394 * @param Way First way
395 * @param Way Second way
396 * @return ArrayList<OsmPrimitive> List of new nodes
397 */
398 private ArrayList<Node> addIntersections(Way a, Way b) {
399 boolean same = a.equals(b);
400 int nodesSizeA = a.getNodesCount();
401 int nodesSizeB = b.getNodesCount();
402
403 ArrayList<Node> nodes = new ArrayList<Node>();
404 ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>();
405 ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>();
406
407 for (int i = (same ? 1 : 0); i < nodesSizeA - 1; i++) {
408 for (int j = (same ? i + 2 : 0); j < nodesSizeB - 1; j++) {
409 // Avoid re-adding nodes that already exist on (some) intersections
410 if(a.getNode(i).equals(b.getNode(j)) || a.getNode(i+1).equals(b.getNode(j))) {
411 nodes.add(b.getNode(j));
412 continue;
413 } else
414 if(a.getNode(i).equals(b.getNode(j+1)) || a.getNode(i+1).equals(b.getNode(j+1))) {
415 nodes.add(b.getNode(j+1));
416 continue;
417 }
418 LatLon intersection = getLineLineIntersection(
419 a.getNode(i) .getEastNorth().east(), a.getNode(i) .getEastNorth().north(),
420 a.getNode(i+1).getEastNorth().east(), a.getNode(i+1).getEastNorth().north(),
421 b.getNode(j) .getEastNorth().east(), b.getNode(j) .getEastNorth().north(),
422 b.getNode(j+1).getEastNorth().east(), b.getNode(j+1).getEastNorth().north());
423 if(intersection == null) {
424 continue;
425 }
426
427 // Create the node. Adding them to the ways must be delayed because we still loop over them
428 Node n = new Node(intersection);
429 cmds.add(new AddCommand(n));
430 nodes.add(n);
431 // The distance is needed to sort and add the nodes in direction of the way
432 nodesA.add(new NodeToSegs(i, n, a.getNode(i).getCoor()));
433 if(same) {
434 nodesA.add(new NodeToSegs(j, n, a.getNode(j).getCoor()));
435 } else {
436 nodesB.add(new NodeToSegs(j, n, b.getNode(j).getCoor()));
437 }
438 }
439 }
440
441 addNodesToWay(a, nodesA);
442 if(!same) {
443 addNodesToWay(b, nodesB);
444 }
445
446 return nodes;
447 }
448
449 /**
450 * Finds the intersection of two lines
451 * @return LatLon null if no intersection was found, the LatLon coordinates of the intersection otherwise
452 */
453 static private LatLon getLineLineIntersection(
454 double x1, double y1, double x2, double y2,
455 double x3, double y3, double x4, double y4) {
456
457 if (!Line2D.linesIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return null;
458
459 // Convert line from (point, point) form to ax+by=c
460 double a1 = y2 - y1;
461 double b1 = x1 - x2;
462 double c1 = x2*y1 - x1*y2;
463
464 double a2 = y4 - y3;
465 double b2 = x3 - x4;
466 double c2 = x4*y3 - x3*y4;
467
468 // Solve the equations
469 double det = a1*b2 - a2*b1;
470 if(det == 0) return null; // Lines are parallel
471
472 return Main.proj.eastNorth2latlon(new EastNorth(
473 (b1*c2 - b2*c1)/det,
474 (a2*c1 -a1*c2)/det
475 ));
476 }
477
478 /**
479 * Inserts given nodes with positions into the given ways
480 * @param Way The way to insert the nodes into
481 * @param Collection<NodeToSegs> The list of nodes with positions to insert
482 */
483 private void addNodesToWay(Way a, ArrayList<NodeToSegs> nodes) {
484 if(nodes.size() == 0)
485 return;
486 Way ax=new Way(a);
487 Collections.sort(nodes);
488
489 int numOfAdds = 1;
490 for(NodeToSegs n : nodes) {
491 ax.addNode(n.pos + numOfAdds, n.n);
492 numOfAdds++;
493 }
494
495 cmds.add(new ChangeCommand(a, ax));
496 }
497
498 /**
499 * Commits the command list with a description
500 * @param String The description of what the commands do
501 */
502 private void commitCommands(String description) {
503 switch(cmds.size()) {
504 case 0:
505 return;
506 case 1:
507 Main.main.undoRedo.add(cmds.getFirst());
508 break;
509 default:
510 Command c = new SequenceCommand(tr(description), cmds);
511 Main.main.undoRedo.add(c);
512 break;
513 }
514
515 cmds.clear();
516 cmdsCount++;
517 }
518
519 /**
520 * Removes a given OsmPrimitive from all relations
521 * @param OsmPrimitive Element to remove from all relations
522 * @return ArrayList<RelationRole> List of relations with roles the primitives was part of
523 */
524 private ArrayList<RelationRole> removeFromRelations(OsmPrimitive osm) {
525 ArrayList<RelationRole> result = new ArrayList<RelationRole>();
526 for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
527 if (r.isDeleted()) {
528 continue;
529 }
530 for (RelationMember rm : r.getMembers()) {
531 if (rm.getMember() != osm) {
532 continue;
533 }
534
535 Relation newRel = new Relation(r);
536 List<RelationMember> members = newRel.getMembers();
537 members.remove(rm);
538 newRel.setMembers(members);
539
540 cmds.add(new ChangeCommand(r, newRel));
541 RelationRole saverel = new RelationRole(r, rm.getRole());
542 if(!result.contains(saverel)) {
543 result.add(saverel);
544 }
545 break;
546 }
547 }
548
549 commitCommands(marktr("Removed Element from Relations"));
550 return result;
551 }
552
553 /**
554 * This method splits ways into smaller parts, using the prepared nodes list as split points.
555 * Uses SplitWayAction.splitWay for the heavy lifting.
556 * @return list of split ways (or original ways if no splitting is done).
557 */
558 private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) {
559
560 ArrayList<Way> result = new ArrayList<Way>();
561 List<Way> ways = new ArrayList<Way>();
562 ways.add(a);
563 ways.add(b);
564
565 for (Way way: ways) {
566 List<List<Node>> chunks = buildNodeChunks(way, nodes);
567 SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
568
569 //execute the command, we need the results
570 Main.main.undoRedo.add(split.getCommand());
571 cmdsCount ++;
572
573 result.add(split.getOriginalWay());
574 result.addAll(split.getNewWays());
575 }
576
577 return result;
578 }
579
580 /**
581 * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on.
582 * @param way the way to chunk
583 * @param splitNodes the places where to cut.
584 * @return list of node segments to produce.
585 */
586 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes)
587 {
588 List<List<Node>> result = new ArrayList<List<Node>>();
589 List<Node> curList = new ArrayList<Node>();
590
591 for(Node node: way.getNodes()){
592 curList.add(node);
593 if (curList.size() > 1 && splitNodes.contains(node)){
594 result.add(curList);
595 curList = new ArrayList<Node>();
596 curList.add(node);
597 }
598 }
599
600 if (curList.size() > 1)
601 {
602 result.add(curList);
603 }
604
605 return result;
606 }
607
608 /**
609 * Returns all nodes for given ways
610 * @param Collection<Way> The list of ways which nodes are to be returned
611 * @return Collection<Node> The list of nodes the ways contain
612 */
613 private Collection<Node> getNodesFromWays(Collection<Way> ways) {
614 Collection<Node> allNodes = new ArrayList<Node>();
615 for(Way w: ways) {
616 allNodes.addAll(w.getNodes());
617 }
618 return allNodes;
619 }
620
621 /**
622 * Gets all inner ways given all ways and outer ways.
623 * @param multigonWays
624 * @param outerWays
625 * @return list of inner ways.
626 */
627 private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) {
628 ArrayList<Way> innerWays = new ArrayList<Way>();
629 Set<Way> outerSet = new HashSet<Way>();
630
631 for(WayInPath w: outerWays) {
632 outerSet.add(w.way);
633 }
634
635 for(Way way: multigonWays) {
636 if (!outerSet.contains(way)) {
637 innerWays.add(way);
638 }
639 }
640
641 return innerWays;
642 }
643
644
645 /**
646 * Finds all ways for a given list of Ways that form the outer hull.
647 * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path.
648 * Prerequisites - the ways must not intersect and have common end nodes where they meet.
649 * @param Collection<Way> A list of (splitted) ways that form a multigon
650 * @return Collection<Way> A list of ways that form the outer boundary of the multigon.
651 */
652 private static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) {
653
654 //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?)
655 Way bestWay = null;
656 Node topNode = null;
657 int topIndex = 0;
658 double minLat = Double.POSITIVE_INFINITY;
659
660 for(Way way: multigonWays) {
661 for (int pos = 0; pos < way.getNodesCount(); pos ++) {
662 Node node = way.getNode(pos);
663
664 if (node.getCoor().lat() < minLat) {
665 minLat = node.getCoor().lat();
666 bestWay = way;
667 topNode = node;
668 topIndex = pos;
669 }
670 }
671 }
672
673 //get two final nodes from best way to mark as starting point and orientation.
674 Node headNode = null;
675 Node prevNode = null;
676
677 if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) {
678 //node is in split point
679 headNode = topNode;
680 //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths.
681 prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon()));
682 } else {
683 //node is inside way - pick the clockwise going end.
684 Node prev = bestWay.getNode(topIndex - 1);
685 Node next = bestWay.getNode(topIndex + 1);
686
687 if (angleIsClockwise(prev, topNode, next)) {
688 headNode = bestWay.lastNode();
689 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2);
690 }
691 else {
692 headNode = bestWay.firstNode();
693 prevNode = bestWay.getNode(1);
694 }
695 }
696
697 Set<Way> outerWays = new HashSet<Way>();
698 ArrayList<WayInPath> result = new ArrayList<WayInPath>();
699
700 //iterate till full circle is reached
701 while (true) {
702
703 bestWay = null;
704 Node bestWayNextNode = null;
705 boolean bestWayReverse = false;
706
707 for (Way way: multigonWays) {
708 boolean wayReverse;
709 Node nextNode;
710
711 if (way.firstNode().equals(headNode)) {
712 //start adjacent to headNode
713 nextNode = way.getNode(1);
714 wayReverse = false;
715
716 if (nextNode.equals(prevNode)) {
717 //this is the path we came from - ignore it.
718 } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
719 //the new way is better
720 bestWay = way;
721 bestWayReverse = wayReverse;
722 bestWayNextNode = nextNode;
723 }
724 }
725
726 if (way.lastNode().equals(headNode)) {
727 //end adjacent to headNode
728 nextNode = way.getNode(way.getNodesCount() - 2);
729 wayReverse = true;
730
731 if (nextNode.equals(prevNode)) {
732 //this is the path we came from - ignore it.
733 } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
734 //the new way is better
735 bestWay = way;
736 bestWayReverse = wayReverse;
737 bestWayNextNode = nextNode;
738 }
739 }
740 }
741
742 if (bestWay == null)
743 throw new RuntimeException();
744 else if (outerWays.contains(bestWay))
745 break; //full circle reached, terminate.
746 else {
747 //add to outer ways, repeat.
748 outerWays.add(bestWay);
749 result.add(new WayInPath(bestWay, bestWayReverse));
750 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode();
751 prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2);
752 }
753 }
754
755 return result;
756 }
757
758 /**
759 * Tests if given point is to the right side of path consisting of 3 points.
760 * @param lineP1 first point in path
761 * @param lineP2 second point in path
762 * @param lineP3 third point in path
763 * @param testPoint
764 * @return true if to the right side, false otherwise
765 */
766 public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint)
767 {
768 boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3);
769 boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint);
770 boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint);
771
772 if (pathBendToRight)
773 return rightOfSeg1 && rightOfSeg2;
774 else
775 return !(!rightOfSeg1 && !rightOfSeg2);
776 }
777
778 /**
779 * This method tests if secondNode is clockwise to first node.
780 * @param commonNode starting point for both vectors
781 * @param firstNode first vector end node
782 * @param secondNode second vector end node
783 * @return true if first vector is clockwise before second vector.
784 */
785 public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode)
786 {
787 double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat());
788 double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat());
789 double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon());
790 double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon());
791
792 return dla1 * dlo2 - dlo1 * dla2 > 0;
793 }
794
795 /**
796 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner.
797 * @param polygonNodes list of nodes from polygon path.
798 * @param point the point to test
799 * @return true if the point is inside polygon.
800 * FIXME: this should probably be moved to tools..
801 */
802 public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point)
803 {
804 if (polygonNodes.size() < 3)
805 return false;
806
807 boolean inside = false;
808 Node p1, p2;
809
810 //iterate each side of the polygon, start with the last segment
811 Node oldPoint = polygonNodes.get(polygonNodes.size() - 1);
812
813 for(Node newPoint: polygonNodes)
814 {
815 //skip duplicate points
816 if (newPoint.equals(oldPoint)) {
817 continue;
818 }
819
820 //order points so p1.lat <= p2.lat;
821 if (newPoint.getCoor().lat() > oldPoint.getCoor().lat())
822 {
823 p1 = oldPoint;
824 p2 = newPoint;
825 }
826 else
827 {
828 p1 = newPoint;
829 p2 = oldPoint;
830 }
831
832 //test if the line is crossed and if so invert the inside flag.
833 if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat())
834 && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat())
835 < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat()))
836 {
837 inside = !inside;
838 }
839
840 oldPoint = newPoint;
841 }
842
843 return inside;
844 }
845
846 /**
847 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
848 * @param Collection<Way> The list of outer ways that belong to that multigon.
849 * @return Way The newly created outer way
850 */
851 private Way joinOuterWays(ArrayList<WayInPath> outerWays) {
852
853 //leave original orientation, if all paths are reverse.
854 boolean allReverse = true;
855 for(WayInPath way: outerWays) {
856 allReverse &= way.insideToTheLeft;
857 }
858
859 if (allReverse) {
860 for(WayInPath way: outerWays){
861 way.insideToTheLeft = !way.insideToTheLeft;
862 }
863 }
864
865 commitCommands(marktr("Join Areas: Remove Short Ways"));
866 Way joinedWay = joinOrientedWays(outerWays);
867 if (joinedWay != null)
868 return closeWay(joinedWay);
869 else
870 return null;
871 }
872
873 /**
874 * Ensures a way is closed. If it isn't, last and first node are connected.
875 * @param Way the way to ensure it's closed
876 * @return Way The joined way.
877 */
878 private Way closeWay(Way w) {
879 if(w.isClosed())
880 return w;
881 Main.main.getCurrentDataSet().setSelected(w);
882 Way wnew = new Way(w);
883 wnew.addNode(wnew.firstNode());
884 cmds.add(new ChangeCommand(w, wnew));
885 commitCommands(marktr("Closed Way"));
886 return (Way)(Main.main.getCurrentDataSet().getSelectedWays().toArray())[0];
887 }
888
889 /**
890 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
891 * @param ArrayList<Way> The list of ways to join and reverse
892 * @return Way The newly created way
893 */
894 private Way joinOrientedWays(ArrayList<WayInPath> ways) {
895 if(ways.size() < 2)
896 return ways.get(0).way;
897
898 // This will turn ways so all of them point in the same direction and CombineAction won't bug
899 // the user about this.
900
901 List<Way> actionWays = new ArrayList<Way>(ways.size());
902
903 for(WayInPath way : ways) {
904 actionWays.add(way.way);
905
906 if (way.insideToTheLeft) {
907 Main.main.getCurrentDataSet().setSelected(way.way);
908 new ReverseWayAction().actionPerformed(null);
909 cmdsCount++;
910 }
911 }
912
913 Way result = new CombineWayAction().combineWays(actionWays);
914
915 if(result != null) {
916 cmdsCount++;
917 }
918 return result;
919 }
920
921 /**
922 * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former)
923 * @param ArrayList<Way> The list of ways to join
924 * @return Way The newly created way
925 */
926 private Way joinWays(ArrayList<Way> ways) {
927 if(ways.size() < 2)
928 return ways.get(0);
929
930 // This will turn ways so all of them point in the same direction and CombineAction won't bug
931 // the user about this.
932 Way a = null;
933 for (Way b : ways) {
934 if(a == null) {
935 a = b;
936 continue;
937 }
938 if(a.getNode(0).equals(b.getNode(0)) ||
939 a.getNode(a.getNodesCount()-1).equals(b.getNode(b.getNodesCount()-1))) {
940 Main.main.getCurrentDataSet().setSelected(b);
941 new ReverseWayAction().actionPerformed(null);
942 cmdsCount++;
943 }
944 a = b;
945 }
946 if ((a = new CombineWayAction().combineWays(ways)) != null) {
947 cmdsCount++;
948 }
949 return a;
950 }
951
952 /**
953 * Finds all ways that may be part of a multipolygon relation and removes them from the given list.
954 * It will automatically combine "good" ways
955 * @param Collection<Way> The list of inner ways to check
956 * @param Way The newly created outer way
957 * @return ArrayList<Way> The List of newly created inner ways
958 */
959 private ArrayList<Way> fixMultipolygons(Collection<Way> uninterestingWays, Way outerWay, boolean selfintersect) {
960 Collection<Node> innerNodes = getNodesFromWays(uninterestingWays);
961 Collection<Node> outerNodes = outerWay.getNodes();
962
963 // The newly created inner ways. uninterestingWays is passed by reference and therefore modified in-place
964 ArrayList<Way> newInnerWays = new ArrayList<Way>();
965
966 // Now we need to find all inner ways that contain a remaining node, but no outer nodes
967 // Remaining nodes are those that contain to more than one way. All nodes that belong to an
968 // inner multigon part will have at least two ways, so we can use this to find which ways do
969 // belong to the multigon.
970 ArrayList<Way> possibleWays = new ArrayList<Way>();
971 wayIterator: for(Way w : uninterestingWays) {
972 boolean hasInnerNodes = false;
973 for(Node n : w.getNodes()) {
974 if(outerNodes.contains(n)) {
975 if(!selfintersect) { // allow outer point for self intersection
976 continue wayIterator;
977 }
978 }
979 else if(!hasInnerNodes && innerNodes.contains(n)) {
980 hasInnerNodes = true;
981 }
982 }
983 if(!hasInnerNodes || w.getNodesCount() < 2) {
984 continue;
985 }
986 possibleWays.add(w);
987 }
988
989 // This removes unnecessary ways that might have been added.
990 removeAlmostAlikeWays(possibleWays);
991
992 // loop twice
993 // in k == 0 prefer ways which allow no Y-joining (i.e. which have only 1 solution)
994 for(int k = 0; k < 2; ++k)
995 {
996 // Join all ways that have one start/ending node in common
997 Way joined = null;
998 outerIterator: do {
999 removePartlyUnconnectedWays(possibleWays);
1000 joined = null;
1001 for(Way w1 : possibleWays) {
1002 if(w1.isClosed()) {
1003 if(!wayIsCollapsed(w1)) {
1004 uninterestingWays.remove(w1);
1005 newInnerWays.add(w1);
1006 }
1007 joined = w1;
1008 possibleWays.remove(w1);
1009 continue outerIterator;
1010 }
1011 ArrayList<Way> secondary = new ArrayList<Way>();
1012 for(Way w2 : possibleWays) {
1013 int i = 0;
1014 // w2 cannot be closed, otherwise it would have been removed above
1015 if(w1.equals(w2)) {
1016 continue;
1017 }
1018 if(w2.isFirstLastNode(w1.firstNode())) {
1019 ++i;
1020 }
1021 if(w2.isFirstLastNode(w1.lastNode())) {
1022 ++i;
1023 }
1024 if(i == 2) // this way closes w1 - take it!
1025 {
1026 if(secondary.size() > 0) {
1027 secondary.clear();
1028 }
1029 secondary.add(w2);
1030 break;
1031 }
1032 else if(i > 0) {
1033 secondary.add(w2);
1034 }
1035 }
1036 if(k == 0 ? secondary.size() == 1 : secondary.size() > 0)
1037 {
1038 ArrayList<Way> joinThem = new ArrayList<Way>();
1039 joinThem.add(w1);
1040 joinThem.add(secondary.get(0));
1041 // Although we joined the ways, we cannot simply assume that they are closed
1042 if((joined = joinWays(joinThem)) != null)
1043 {
1044 uninterestingWays.removeAll(joinThem);
1045 possibleWays.removeAll(joinThem);
1046
1047 //List<Node> nodes = joined.getNodes();
1048 // check if we added too much
1049 /*for(int i = 1; i < nodes.size()-2; ++i)
1050 {
1051 if(nodes.get(i) == nodes.get(nodes.size()-1))
1052 System.out.println("Joining of ways produced unexpecteded result\n");
1053 }*/
1054 uninterestingWays.add(joined);
1055 possibleWays.add(joined);
1056 continue outerIterator;
1057 }
1058 }
1059 }
1060 } while(joined != null);
1061 }
1062 return newInnerWays;
1063 }
1064
1065 /**
1066 * Removes almost alike ways (= ways that are on top of each other for all nodes)
1067 * @param ArrayList<Way> the ways to remove almost-duplicates from
1068 */
1069 private void removeAlmostAlikeWays(ArrayList<Way> ways) {
1070 Collection<Way> removables = new ArrayList<Way>();
1071 outer: for(int i=0; i < ways.size(); i++) {
1072 Way a = ways.get(i);
1073 for(int j=i+1; j < ways.size(); j++) {
1074 Way b = ways.get(j);
1075 List<Node> revNodes = new ArrayList<Node>(b.getNodes());
1076 Collections.reverse(revNodes);
1077 if(a.getNodes().equals(b.getNodes()) || a.getNodes().equals(revNodes)) {
1078 removables.add(a);
1079 continue outer;
1080 }
1081 }
1082 }
1083 ways.removeAll(removables);
1084 }
1085
1086 /**
1087 * Removes ways from the given list whose starting or ending node doesn't
1088 * connect to other ways from the same list (it's like removing spikes).
1089 * @param ArrayList<Way> The list of ways to remove "spikes" from
1090 */
1091 private void removePartlyUnconnectedWays(ArrayList<Way> ways) {
1092 List<Way> removables = new ArrayList<Way>();
1093 for(Way a : ways) {
1094 if(a.isClosed()) {
1095 continue;
1096 }
1097 boolean connectedStart = false;
1098 boolean connectedEnd = false;
1099 for(Way b : ways) {
1100 if(a.equals(b)) {
1101 continue;
1102 }
1103 if(b.isFirstLastNode(a.firstNode())) {
1104 connectedStart = true;
1105 }
1106 if(b.isFirstLastNode(a.lastNode())) {
1107 connectedEnd = true;
1108 }
1109 }
1110 if(!connectedStart || !connectedEnd) {
1111 removables.add(a);
1112 }
1113 }
1114 ways.removeAll(removables);
1115 }
1116
1117 /**
1118 * Checks if a way is collapsed (i.e. looks like <---->)
1119 * @param Way A *closed* way to check if it is collapsed
1120 * @return boolean If the closed way is collapsed or not
1121 */
1122 private boolean wayIsCollapsed(Way w) {
1123 if(w.getNodesCount() <= 3) return true;
1124
1125 // If a way contains more than one node twice, it must be collapsed (only start/end node may be the same)
1126 Way x = new Way(w);
1127 int count = 0;
1128 for(Node n : w.getNodes()) {
1129 x.removeNode(n);
1130 if(x.containsNode(n)) {
1131 count++;
1132 }
1133 if(count == 2) return true;
1134 }
1135 return false;
1136 }
1137
1138 /**
1139 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1140 * @param Collection<Way> List of already closed inner ways
1141 * @param Way The outer way
1142 * @param ArrayList<RelationRole> The list of relation with roles to add own relation to
1143 */
1144 private void addOwnMultigonRelation(Collection<Way> inner, Way outer, ArrayList<RelationRole> rels) {
1145 if(inner.size() == 0) return;
1146 // Create new multipolygon relation and add all inner ways to it
1147 Relation newRel = new Relation();
1148 newRel.put("type", "multipolygon");
1149 for(Way w : inner) {
1150 newRel.addMember(new RelationMember("inner", w));
1151 }
1152 cmds.add(new AddCommand(newRel));
1153
1154 // We don't add outer to the relation because it will be handed to fixRelations()
1155 // which will then do the remaining work. Collections are passed by reference, so no
1156 // need to return it
1157 rels.add(new RelationRole(newRel, "outer"));
1158 //return rels;
1159 }
1160
1161 /**
1162 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1163 * relations where the joined areas were in "outer" role a new relation is created instead with all
1164 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1165 * @param ArrayList<RelationRole> List of relations with roles the (original) ways were part of
1166 * @param Way The newly created outer area/way
1167 */
1168 private void fixRelations(ArrayList<RelationRole> rels, Way outer) {
1169 ArrayList<RelationRole> multiouters = new ArrayList<RelationRole>();
1170 for(RelationRole r : rels) {
1171 if( r.rel.get("type") != null &&
1172 r.rel.get("type").equalsIgnoreCase("multipolygon") &&
1173 r.role.equalsIgnoreCase("outer")
1174 ) {
1175 multiouters.add(r);
1176 continue;
1177 }
1178 // Add it back!
1179 Relation newRel = new Relation(r.rel);
1180 newRel.addMember(new RelationMember(r.role, outer));
1181 cmds.add(new ChangeCommand(r.rel, newRel));
1182 }
1183
1184 Relation newRel = null;
1185 switch(multiouters.size()) {
1186 case 0:
1187 return;
1188 case 1:
1189 // Found only one to be part of a multipolygon relation, so just add it back as well
1190 newRel = new Relation(multiouters.get(0).rel);
1191 newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1192 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1193 return;
1194 default:
1195 // Create a new relation with all previous members and (Way)outer as outer.
1196 newRel = new Relation();
1197 for(RelationRole r : multiouters) {
1198 // Add members
1199 for(RelationMember rm : r.rel.getMembers())
1200 if(!newRel.getMembers().contains(rm)) {
1201 newRel.addMember(rm);
1202 }
1203 // Add tags
1204 for (String key : r.rel.keySet()) {
1205 newRel.put(key, r.rel.get(key));
1206 }
1207 // Delete old relation
1208 cmds.add(new DeleteCommand(r.rel));
1209 }
1210 newRel.addMember(new RelationMember("outer", outer));
1211 cmds.add(new AddCommand(newRel));
1212 }
1213 }
1214
1215 /**
1216 * @param Collection<Way> The List of Ways to remove all tags from
1217 */
1218 private void stripTags(Collection<Way> ways) {
1219 for(Way w: ways) {
1220 stripTags(w);
1221 }
1222 commitCommands(marktr("Remove tags from inner ways"));
1223 }
1224
1225 /**
1226 * @param Way The Way to remove all tags from
1227 */
1228 private void stripTags(Way x) {
1229 if(x.getKeys() == null) return;
1230 Way y = new Way(x);
1231 for (String key : x.keySet()) {
1232 y.remove(key);
1233 }
1234 cmds.add(new ChangeCommand(x, y));
1235 }
1236
1237 /**
1238 * Takes the last cmdsCount actions back and combines them into a single action
1239 * (for when the user wants to undo the join action)
1240 * @param String The commit message to display
1241 */
1242 private void makeCommitsOneAction(String message) {
1243 UndoRedoHandler ur = Main.main.undoRedo;
1244 cmds.clear();
1245 int i = Math.max(ur.commands.size() - cmdsCount, 0);
1246 for(; i < ur.commands.size(); i++) {
1247 cmds.add(ur.commands.get(i));
1248 }
1249
1250 for(i = 0; i < cmds.size(); i++) {
1251 ur.undo();
1252 }
1253
1254 commitCommands(message == null ? marktr("Join Areas Function") : message);
1255 cmdsCount = 0;
1256 }
1257
1258 @Override
1259 protected void updateEnabledState() {
1260 if (getCurrentDataSet() == null) {
1261 setEnabled(false);
1262 } else {
1263 updateEnabledState(getCurrentDataSet().getSelected());
1264 }
1265 }
1266
1267 @Override
1268 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1269 setEnabled(selection != null && !selection.isEmpty());
1270 }
1271}
Note: See TracBrowser for help on using the repository browser.