package mergeoverlap; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.applyAutomaticTagConflictResolution; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing; import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JOptionPane; import mergeoverlap.hack.MyCombinePrimitiveResolverDialog; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.actions.SplitWayAction; import org.openstreetmap.josm.actions.CombineWayAction.NodeGraph; import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.corrector.ReverseWayTagCorrector; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.TagCollection; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.Shortcut; import org.openstreetmap.josm.tools.UserCancelException; /** * Merge overlapping part of ways. */ public class MergeOverlapAction extends JosmAction { Map<Way, List<Relation>> relations = new HashMap<>(); Map<Way, Way> oldWays = new HashMap<>(); Map<Relation, Relation> newRelations = new HashMap<>(); Set<Way> deletes = new HashSet<>(); /** * Constructs a new {@code MergeOverlapAction}. */ public MergeOverlapAction() { super(tr("Merge overlap"), "merge_overlap", tr("Merge overlap of ways."), Shortcut.registerShortcut("tools:mergeoverlap",tr("Tool: {0}", tr("Merge overlap")), KeyEvent.VK_O, Shortcut.ALT_CTRL), true); } /** * The action button has been clicked * * @param e * Action Event */ @Override public void actionPerformed(ActionEvent e) { // List of selected ways List<Way> ways = new ArrayList<>(); relations.clear(); newRelations.clear(); // For every selected way for (OsmPrimitive osm : getLayerManager().getEditDataSet().getSelected()) { if (osm instanceof Way && !osm.isDeleted()) { Way way = (Way) osm; ways.add(way); List<Relation> rels = new ArrayList<>(); for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) { rels.add(r); } relations.put(way, rels); } } List<Way> sel = new ArrayList<>(ways); Collection<Command> cmds = new LinkedList<>(); // ***** // split // ***** for (Way way : ways) { Set<Node> nodes = new HashSet<>(); for (Way opositWay : ways) { if (way != opositWay) { List<NodePos> nodesPos = new LinkedList<>(); int pos = 0; for (Node node : way.getNodes()) { int opositPos = 0; for (Node opositNode : opositWay.getNodes()) { if (node == opositNode) { if (opositWay.isClosed()) { opositPos %= opositWay.getNodesCount() - 1; } nodesPos.add(new NodePos(node, pos, opositPos)); break; } opositPos++; } pos++; } NodePos start = null; NodePos end = null; int increment = 0; boolean hasFirst = false; for (NodePos node : nodesPos) { if (start == null) { start = node; } else { if (end == null) { if (follows(way, opositWay, start, node, 1)) { end = node; increment = +1; } else if (follows(way, opositWay, start, node, -1)) { end = node; increment = -1; } else { start = node; end = null; } } else { if (follows(way, opositWay, end, node, increment)) { end = node; } else { hasFirst = addNodes(start, end, way, nodes, hasFirst); start = node; end = null; } } } } if (start != null && end != null) { hasFirst = addNodes(start, end, way, nodes, hasFirst); start = null; end = null; } } } if (!nodes.isEmpty() && !way.isClosed() || nodes.size() >= 2) { List<List<Node>> wayChunks = SplitWayAction.buildSplitChunks(way, new ArrayList<>(nodes)); SplitWayResult result = splitWay(getLayerManager().getEditLayer(), way, wayChunks); cmds.add(result.getCommand()); sel.remove(way); sel.add(result.getOriginalWay()); sel.addAll(result.getNewWays()); List<Relation> rels = relations.remove(way); relations.put(result.getOriginalWay(), rels); for (Way w : result.getNewWays()) { relations.put(w, rels); } } } // ***** // merge // ***** ways = new ArrayList<>(sel); while (!ways.isEmpty()) { Way way = ways.get(0); List<Way> combine = new ArrayList<>(); combine.add(way); for (Way opositWay : ways) { if (way != opositWay && way.getNodesCount() == opositWay.getNodesCount()) { boolean equals1 = true; for (int i = 0; i < way.getNodesCount(); i++) { if (way.getNode(i) != opositWay.getNode(i)) { equals1 = false; break; } } boolean equals2 = true; for (int i = 0; i < way.getNodesCount(); i++) { if (way.getNode(i) != opositWay.getNode(way.getNodesCount() - i - 1)) { equals2 = false; break; } } if (equals1 || equals2) { combine.add(opositWay); } } } ways.removeAll(combine); if (combine.size() > 1) { sel.removeAll(combine); // combine Pair<Way, List<Command>> combineResult; try { combineResult = combineWaysWorker(combine); } catch (UserCancelException ex) { return; } sel.add(combineResult.a); cmds.addAll(combineResult.b); } } for (Relation old : newRelations.keySet()) { cmds.add(new ChangeCommand(old, newRelations.get(old))); } List<Way> del = new LinkedList<>(); for (Way w : deletes) { if (w.getDataSet() != null && !w.isDeleted()) { del.add(w); } } if (!del.isEmpty()) { cmds.add(new DeleteCommand(del)); } // Commit Main.main.undoRedo.add(new SequenceCommand(tr("Merge Overlap (combine)"), cmds)); getLayerManager().getEditDataSet().setSelected(sel); Main.map.repaint(); relations.clear(); newRelations.clear(); oldWays.clear(); } private static class NodePos { Node node; int pos; int opositPos; NodePos(Node n, int p, int op) { node = n; pos = p; opositPos = op; } @Override public String toString() { return "NodePos: " + pos + ", " + opositPos + ", " + node; } } private boolean addNodes(NodePos start, NodePos end, Way way, Set<Node> nodes, boolean hasFirst) { if (way.isClosed() || (start.node != way.getNode(0) && start.node != way.getNode(way.getNodesCount() - 1))) { hasFirst = hasFirst || start.node == way.getNode(0); nodes.add(start.node); } if (way.isClosed() || (end.node != way.getNode(0) && end.node != way.getNode(way.getNodesCount() - 1))) { if (hasFirst && (end.node == way.getNode(way.getNodesCount() - 1))) { nodes.remove(way.getNode(0)); } else { nodes.add(end.node); } } return hasFirst; } private boolean follows(Way way1, Way way2, NodePos np1, NodePos np2, int incr) { if (way2.isClosed() && incr == 1 && np1.opositPos == way2.getNodesCount() - 2) { return np2.pos == np1.pos + 1 && np2.opositPos == 0; } else if (way2.isClosed() && incr == 1 && np1.opositPos == 0) { return np2.pos == np1.pos && np2.opositPos == 0 || np2.pos == np1.pos + 1 && np2.opositPos == 1; } else if (way2.isClosed() && incr == -1 && np1.opositPos == 0) { return np2.pos == np1.pos && np2.opositPos == 0 || np2.pos == np1.pos + 1 && np2.opositPos == way2.getNodesCount() - 2; } else { return np2.pos == np1.pos + 1 && np2.opositPos == np1.opositPos + incr; } } /** * Splits a way * * @param layer * @param way * @param wayChunks * @return */ private SplitWayResult splitWay(OsmDataLayer layer, Way way, List<List<Node>> wayChunks) { // build a list of commands, and also a new selection list Collection<Command> commandList = new ArrayList<>(wayChunks.size()); Iterator<List<Node>> chunkIt = wayChunks.iterator(); Collection<String> nowarnroles = Main.pref.getCollection("way.split.roles.nowarn", Arrays.asList(new String[] { "outer", "inner", "forward", "backward" })); // First, change the original way Way changedWay = new Way(way); oldWays.put(changedWay, way); changedWay.setNodes(chunkIt.next()); commandList.add(new ChangeCommand(way, changedWay)); List<Way> newWays = new ArrayList<>(); // Second, create new ways while (chunkIt.hasNext()) { Way wayToAdd = new Way(); wayToAdd.setKeys(way.getKeys()); newWays.add(wayToAdd); wayToAdd.setNodes(chunkIt.next()); commandList.add(new AddCommand(layer, wayToAdd)); } boolean warnmerole = false; boolean warnme = false; // now copy all relations to new way also for (Relation r : getParentRelations(way)) { if (!r.isUsable()) { continue; } Relation c = null; String type = r.get("type"); if (type == null) { type = ""; } int ic = 0, ir = 0; List<RelationMember> relationMembers = r.getMembers(); for (RelationMember rm : relationMembers) { if (rm.isWay() && rm.getMember() == way) { boolean insert = true; if ("restriction".equals(type)) { /* * this code assumes the restriction is correct. No real * error checking done */ String role = rm.getRole(); if ("from".equals(role) || "to".equals(role)) { OsmPrimitive via = null; for (RelationMember rmv : r.getMembers()) { if ("via".equals(rmv.getRole())) { via = rmv.getMember(); } } List<Node> nodes = new ArrayList<>(); if (via != null) { if (via instanceof Node) { nodes.add((Node) via); } else if (via instanceof Way) { nodes.add(((Way) via).lastNode()); nodes.add(((Way) via).firstNode()); } } Way res = null; for (Node n : nodes) { if (changedWay.isFirstLastNode(n)) { res = way; } } if (res == null) { for (Way wayToAdd : newWays) { for (Node n : nodes) { if (wayToAdd.isFirstLastNode(n)) { res = wayToAdd; } } } if (res != null) { if (c == null) { c = getNew(r); } c.addMember(new RelationMember(role, res)); c.removeMembersFor(way); insert = false; } } else { insert = false; } } else if (!"via".equals(role)) { warnme = true; } } else if (!("route".equals(type)) && !("multipolygon".equals(type))) { warnme = true; } if (c == null) { c = getNew(r); } if (insert) { if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) { warnmerole = true; } Boolean backwards = null; int k = 1; while (ir - k >= 0 || ir + k < relationMembers.size()) { if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) { Way w = relationMembers.get(ir - k).getWay(); if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { backwards = false; } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { backwards = true; } break; } if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) { Way w = relationMembers.get(ir + k).getWay(); if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) { backwards = true; } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) { backwards = false; } break; } k++; } int j = ic; for (Way wayToAdd : newWays) { RelationMember em = new RelationMember(rm.getRole(), wayToAdd); j++; if ((backwards != null) && backwards) { c.addMember(ic, em); } else { c.addMember(j, em); } } ic = j; } } ic++; ir++; } if (c != null) { // commandList.add(new ChangeCommand(layer, r, c)); newRelations.put(r, c); } } if (warnmerole) { JOptionPane.showMessageDialog(Main.parent, tr("<html>A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"), tr("Warning"), JOptionPane.WARNING_MESSAGE); } else if (warnme) { JOptionPane.showMessageDialog(Main.parent, tr("<html>A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary.</html>"), tr("Warning"), JOptionPane.WARNING_MESSAGE); } return new SplitWayResult(new SequenceCommand(tr("Split way"), commandList), null, changedWay, newWays); } /** * @param ways * @return null if ways cannot be combined. Otherwise returns the combined * ways and the commands to combine * @throws UserCancelException */ private Pair<Way, List<Command>> combineWaysWorker(Collection<Way> ways) throws UserCancelException { // prepare and clean the list of ways to combine if (ways == null || ways.isEmpty()) return null; ways.remove(null); // just in case - remove all null ways from the collection // remove duplicates, preserving order ways = new LinkedHashSet<>(ways); // try to build a new way which includes all the combined ways NodeGraph graph = NodeGraph.createUndirectedGraphFromNodeWays(ways); List<Node> path = graph.buildSpanningPath(); // check whether any ways have been reversed in the process // and build the collection of tags used by the ways to combine TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways); List<Way> reversedWays = new LinkedList<>(); List<Way> unreversedWays = new LinkedList<>(); for (Way w : ways) { if ((path.indexOf(w.getNode(0)) + 1) == path.lastIndexOf(w.getNode(1))) { unreversedWays.add(w); } else { reversedWays.add(w); } } // reverse path if all ways have been reversed if (unreversedWays.isEmpty()) { Collections.reverse(path); unreversedWays = reversedWays; reversedWays = null; } if ((reversedWays != null) && !reversedWays.isEmpty()) { // filter out ways that have no direction-dependent tags unreversedWays = ReverseWayTagCorrector.irreversibleWays(unreversedWays); reversedWays = ReverseWayTagCorrector.irreversibleWays(reversedWays); // reverse path if there are more reversed than unreversed ways with // direction-dependent tags if (reversedWays.size() > unreversedWays.size()) { Collections.reverse(path); List<Way> tempWays = unreversedWays; unreversedWays = reversedWays; reversedWays = tempWays; } // if there are still reversed ways with direction-dependent tags, // reverse their tags if (!reversedWays.isEmpty()) { List<Way> unreversedTagWays = new ArrayList<>(ways); unreversedTagWays.removeAll(reversedWays); ReverseWayTagCorrector reverseWayTagCorrector = new ReverseWayTagCorrector(); List<Way> reversedTagWays = new ArrayList<>(); Collection<Command> changePropertyCommands = null; for (Way w : reversedWays) { Way wnew = new Way(w); reversedTagWays.add(wnew); changePropertyCommands = reverseWayTagCorrector.execute(w, wnew); } if ((changePropertyCommands != null) && !changePropertyCommands.isEmpty()) { for (Command c : changePropertyCommands) { c.executeCommand(); } } wayTags = TagCollection.unionOfAllPrimitives(reversedTagWays); wayTags.add(TagCollection.unionOfAllPrimitives(unreversedTagWays)); } } // create the new way and apply the new node list Way targetWay = getTargetWay(ways); Way modifiedTargetWay = new Way(targetWay); modifiedTargetWay.setNodes(path); TagCollection completeWayTags = new TagCollection(wayTags); applyAutomaticTagConflictResolution(completeWayTags); normalizeTagCollectionBeforeEditing(completeWayTags, ways); TagCollection tagsToEdit = new TagCollection(completeWayTags); completeTagCollectionForEditing(tagsToEdit); MyCombinePrimitiveResolverDialog dialog = MyCombinePrimitiveResolverDialog.getInstance(); dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues()); dialog.setTargetPrimitive(targetWay); Set<Relation> parentRelations = getParentRelations(ways); dialog.getRelationMemberConflictResolverModel().populate(parentRelations, ways, oldWays); dialog.prepareDefaultDecisions(); // resolve tag conflicts if necessary if (askForMergeTag(ways) || duplicateParentRelations(ways)) { dialog.setVisible(true); if (!dialog.isApplied()) throw new UserCancelException(); } List<Command> cmds = new LinkedList<>(); deletes.addAll(ways); deletes.remove(targetWay); cmds.add(new ChangeCommand(targetWay, modifiedTargetWay)); cmds.addAll(dialog.buildWayResolutionCommands()); dialog.buildRelationCorrespondance(newRelations, oldWays); return new Pair<>(targetWay, cmds); } private static Way getTargetWay(Collection<Way> combinedWays) { // init with an arbitrary way Way targetWay = combinedWays.iterator().next(); // look for the first way already existing on the server for (Way w : combinedWays) { targetWay = w; if (!w.isNew()) { break; } } return targetWay; } /** * @return has tag to be merged (=> ask) */ private static boolean askForMergeTag(Collection<Way> ways) { for (Way way : ways) { for (Way oposite : ways) { for (String key : way.getKeys().keySet()) { if (!"source".equals(key) && oposite.hasKey(key) && !way.get(key).equals(oposite.get(key))) { return true; } } } } return false; } /** * @return has duplicate parent relation */ private boolean duplicateParentRelations(Collection<Way> ways) { Set<Relation> relations = new HashSet<>(); for (Way w : ways) { List<Relation> rs = getParentRelations(w); for (Relation r : rs) { if (relations.contains(r)) { return true; } } relations.addAll(rs); } return false; } /** * Replies the set of referring relations * * @return the set of referring relations */ private List<Relation> getParentRelations(Way way) { List<Relation> rels = new ArrayList<>(); for (Relation r : relations.get(way)) { if (newRelations.containsKey(r)) { rels.add(newRelations.get(r)); } else { rels.add(r); } } return rels; } private Relation getNew(Relation r) { return getNew(r, newRelations); } public static Relation getNew(Relation r, Map<Relation, Relation> newRelations) { if (newRelations.containsValue(r)) { return r; } else { Relation c = new Relation(r); newRelations.put(r, c); return c; } } /* private Way getOld(Way r) { return getOld(r, oldWays); }*/ public static Way getOld(Way w, Map<Way, Way> oldWays) { if (oldWays.containsKey(w)) { return oldWays.get(w); } else { return w; } } /** * Replies the set of referring relations * * @return the set of referring relations */ private Set<Relation> getParentRelations(Collection<Way> ways) { Set<Relation> ret = new HashSet<>(); for (Way w : ways) { ret.addAll(getParentRelations(w)); } return ret; } /** Enable this action only if something is selected */ @Override protected void updateEnabledState() { if (getLayerManager().getEditDataSet() == null) { setEnabled(false); } else { updateEnabledState(getLayerManager().getEditDataSet().getSelected()); } } /** Enable this action only if something is selected */ @Override protected void updateEnabledState( Collection<? extends OsmPrimitive> selection) { if (selection == null) { setEnabled(false); return; } for (OsmPrimitive primitive : selection) { if (!(primitive instanceof Way) || primitive.isDeleted()) { setEnabled(false); return; } } setEnabled(selection.size() >= 2); } }