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);
}
}