// License: GPL. For details, see LICENSE file.
package reverter;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.conflict.ConflictCollection;
import org.openstreetmap.josm.data.osm.DataSet;
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.Way;
/**
* Modified {@see org.openstreetmap.josm.data.osm.DataSetMerger} that
* produces list of commands instead of directly merging layers.
*
*/
final class DataSetCommandMerger {
/** the collection of conflicts created during merging */
private final ConflictCollection conflicts = new ConflictCollection();
/** the source dataset where primitives are merged from */
private final DataSet sourceDataSet;
private final DataSet targetDataSet;
private final List<Command> cmds = new LinkedList<>();
private final List<OsmPrimitive> nominalRevertedPrimitives = new LinkedList<>();
/**
* constructor
*/
DataSetCommandMerger(DataSet sourceDataSet, DataSet targetDataSet) {
this.sourceDataSet = sourceDataSet;
this.targetDataSet = targetDataSet;
merge();
}
private void addChangeCommandIfNotEquals(OsmPrimitive target, OsmPrimitive newTarget, boolean nominal) {
if (!target.hasEqualSemanticAttributes(newTarget)) {
cmds.add(new ChangeCommand(target, newTarget));
if (nominal) {
nominalRevertedPrimitives.add(target);
}
Main.debug("Reverting "+target+" to "+newTarget);
}
}
private OsmPrimitive getMergeTarget(OsmPrimitive mergeSource) {
OsmPrimitive p = targetDataSet.getPrimitiveById(mergeSource.getId(), mergeSource.getType());
if (p == null)
throw new IllegalStateException(tr("Missing merge target of type {0} with id {1}",
mergeSource.getType(), mergeSource.getUniqueId()));
return p;
}
private void mergePrimitive(OsmPrimitive source, OsmPrimitive target, OsmPrimitive newTarget) {
newTarget.mergeFrom(source);
newTarget.setOsmId(target.getId(), target.getVersion());
newTarget.setVisible(target.isVisible());
newTarget.setDeleted(false);
}
/**
* Merges the source node onto its target node.
*
* @param source the source way
*/
private void mergeNode(Node source) {
if (source.isIncomplete()) return;
if (!source.isVisible()) return;
Node target = (Node) getMergeTarget(source);
Node newTarget = new Node(target);
mergePrimitive(source, target, newTarget);
addChangeCommandIfNotEquals(target, newTarget, true);
}
/**
* Merges the source way onto its target way.
*
* @param source the source way
* @throws IllegalStateException thrown if no target way can be found for the source way
* @throws IllegalStateException thrown if there isn't a target node for one of the nodes in the source way
*
*/
private void mergeWay(Way source) throws IllegalStateException {
if (source.isIncomplete()) return;
if (!source.isVisible()) return;
Way target = (Way) getMergeTarget(source);
// use a set to avoid conflicts being added twice for closed ways, fixes #11811
Collection<Conflict<OsmPrimitive>> localConflicts = new LinkedHashSet<>();
List<Node> newNodes = new ArrayList<>(source.getNodesCount());
for (Node sourceNode : source.getNodes()) {
Node targetNode = (Node) getMergeTarget(sourceNode);
// Target node is not deleted or it will be undeleted when running existing commands
if (!targetNode.isDeleted() || nominalRevertedPrimitives.contains(targetNode)) {
newNodes.add(targetNode);
// Target node has been deleted by a more recent changeset -> conflict
} else if (sourceNode.isIncomplete() && !conflicts.hasConflictForMy(targetNode)) {
localConflicts.add(new Conflict<OsmPrimitive>(targetNode, sourceNode, true));
} else {
Main.info("Skipping target node "+targetNode+" for source node "+sourceNode+" while reverting way "+source);
}
}
Way newTarget = new Way(target);
mergePrimitive(source, target, newTarget);
newTarget.setNodes(newNodes);
if (newNodes.isEmpty()) {
Main.error("Unable to revert "+source+" as it produces 0 nodes way "+newTarget);
} else {
for (Conflict<OsmPrimitive> c : localConflicts) {
Main.warn("New conflict: "+c);
conflicts.add(c);
Node targetNode = (Node) c.getTheir();
Node undeletedTargetNode = new Node(targetNode);
undeletedTargetNode.setDeleted(false);
addChangeCommandIfNotEquals(targetNode, undeletedTargetNode, false);
}
addChangeCommandIfNotEquals(target, newTarget, true);
}
}
/**
* Merges the source relation onto the corresponding target relation.
* @param source the source relation
* @throws IllegalStateException thrown if there is no corresponding target relation
* @throws IllegalStateException thrown if there isn't a corresponding target object for one of the relation
* members in source
*/
private void mergeRelation(Relation source) throws IllegalStateException {
if (source.isIncomplete()) return;
if (!source.isVisible()) return;
Relation target = (Relation) getMergeTarget(source);
LinkedList<RelationMember> newMembers = new LinkedList<>();
for (RelationMember sourceMember : source.getMembers()) {
OsmPrimitive targetMember = getMergeTarget(sourceMember.getMember());
if (targetMember.isDeleted() && sourceMember.getMember().isIncomplete()
&& !conflicts.hasConflictForMy(targetMember)) {
conflicts.add(new Conflict<>(targetMember, sourceMember.getMember(), true));
OsmPrimitive undeletedTargetMember;
switch(targetMember.getType()) {
case NODE: undeletedTargetMember = new Node((Node) targetMember); break;
case WAY: undeletedTargetMember = new Way((Way) targetMember); break;
case RELATION: undeletedTargetMember = new Relation((Relation) targetMember); break;
default: throw new AssertionError();
}
undeletedTargetMember.setDeleted(false);
addChangeCommandIfNotEquals(targetMember, undeletedTargetMember, false);
}
newMembers.add(new RelationMember(sourceMember.getRole(), targetMember));
}
Relation newRelation = new Relation(target);
mergePrimitive(source, target, newRelation);
newRelation.setMembers(newMembers);
addChangeCommandIfNotEquals(target, newRelation, true);
}
private void merge() {
for (Node node: sourceDataSet.getNodes()) {
mergeNode(node);
}
for (Way way: sourceDataSet.getWays()) {
mergeWay(way);
}
for (Relation relation: sourceDataSet.getRelations()) {
mergeRelation(relation);
}
}
public List<Command> getCommandList() {
return cmds;
}
/**
* replies the map of conflicts
*
* @return the map of conflicts
*/
public ConflictCollection getConflicts() {
return conflicts;
}
}