// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.logging.Logger; import org.openstreetmap.josm.actions.upload.CyclicUploadDependencyException; 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; /** * Represents a collection of {@see OsmPrimitive}s which should be uploaded to the * API. * The collection is derived from the modified primitives of an {@see DataSet} and it provides methods * for sorting the objects in upload order. * */ public class APIDataSet { private LinkedList toAdd; private LinkedList toUpdate; private LinkedList toDelete; /** * creates a new empty data set */ public APIDataSet() { toAdd = new LinkedList(); toUpdate = new LinkedList(); toDelete = new LinkedList(); } /** * initializes the API data set with the modified primitives in ds * * @param ds the data set. Ignored, if null. */ public void init(DataSet ds) { if (ds == null) return; toAdd.clear(); toUpdate.clear(); toDelete.clear(); for (OsmPrimitive osm :ds.allPrimitives()) { if (osm.get("josm/ignore") != null) { continue; } if (osm.isNew() && !osm.isDeleted()) { toAdd.add(osm); } else if (osm.isModified() && !osm.isDeleted()) { toUpdate.add(osm); } else if (osm.isDeleted() && !osm.isNew() && osm.isModified()) { toDelete.add(osm); } } sortDeleted(); sortNew(); } /** * Ensures that primitives are deleted in the following order: Relations, then Ways, * then Nodes. * */ protected void sortDeleted() { Collections.sort( toDelete, new Comparator() { public int compare(OsmPrimitive o1, OsmPrimitive o2) { if (o1 instanceof Node && o2 instanceof Node) return 0; else if (o1 instanceof Node) return 1; else if (o2 instanceof Node) return -1; if (o1 instanceof Way && o2 instanceof Way) return 0; else if (o1 instanceof Way && o2 instanceof Relation) return 1; else if (o2 instanceof Way && o1 instanceof Relation) return -1; return 0; } } ); } /** * Ensures that primitives are added in the following order: Nodes, then Ways, * then Relations. * */ protected void sortNew() { Collections.sort( toAdd, new Comparator() { public int compare(OsmPrimitive o1, OsmPrimitive o2) { if (o1 instanceof Node && o2 instanceof Node) return 0; else if (o1 instanceof Node) return -1; else if (o2 instanceof Node) return 1; if (o1 instanceof Way && o2 instanceof Way) return 0; else if (o1 instanceof Way && o2 instanceof Relation) return -1; else if (o2 instanceof Way && o1 instanceof Relation) return 1; return 0; } } ); } /** * initializes the API data set with the modified primitives in ds * * @param ds the data set. Ignored, if null. */ public APIDataSet(DataSet ds) { this(); init(ds); } /** * initializes the API data set with the primitives in primitives * * @param primitives the collection of primitives */ public APIDataSet(Collection primitives) { this(); toAdd.clear(); toUpdate.clear(); toDelete.clear(); for (OsmPrimitive osm: primitives) { if (osm.isNew() && !osm.isDeleted()) { toAdd.addLast(osm); } else if (osm.isModified() && !osm.isDeleted()) { toUpdate.addLast(osm); } else if (osm.isDeleted() && !osm.isNew() && osm.isModified()) { toDelete.addFirst(osm); } } sortNew(); sortDeleted(); } /** * Replies true if there are no primitives to upload * * @return true if there are no primitives to upload */ public boolean isEmpty() { return toAdd.isEmpty() && toUpdate.isEmpty() && toDelete.isEmpty(); } /** * Replies the primitives which should be added to the OSM database * * @return the primitives which should be added to the OSM database */ public List getPrimitivesToAdd() { return toAdd; } /** * Replies the primitives which should be updated in the OSM database * * @return the primitives which should be updated in the OSM database */ public List getPrimitivesToUpdate() { return toUpdate; } /** * Replies the primitives which should be deleted in the OSM database * * @return the primitives which should be deleted in the OSM database */ public List getPrimitivesToDelete() { return toDelete; } /** * Replies all primitives * * @return all primitives */ public List getPrimitives() { LinkedList ret = new LinkedList(); ret.addAll(toAdd); ret.addAll(toUpdate); ret.addAll(toDelete); return ret; } /** * Replies the number of objects to upload * * @return the number of objects to upload */ public int getSize() { return toAdd.size() + toUpdate.size() + toDelete.size(); } public void removeProcessed(Collection processed) { if (processed == null) return; toAdd.removeAll(processed); toUpdate.removeAll(processed); toDelete.removeAll(processed); } /** * Adjusts the upload order for new relations. Child relations are uploaded first, * parent relations second. * * This method detects cyclic dependencies in new relation. Relations with cyclic * dependencies can't be uploaded. * * @throws CyclicUploadDependencyException thrown, if a cyclic dependency is detected */ public void adjustRelationUploadOrder() throws CyclicUploadDependencyException{ LinkedList newToAdd = new LinkedList(); newToAdd.addAll(OsmPrimitive.getFilteredList(toAdd, Node.class)); newToAdd.addAll(OsmPrimitive.getFilteredList(toAdd, Way.class)); List relationsToAdd = OsmPrimitive.getFilteredList(toAdd, Relation.class); List noProblemRelations = filterRelationsNotReferringToNewRelations(relationsToAdd); newToAdd.addAll(noProblemRelations); relationsToAdd.removeAll(noProblemRelations); RelationUploadDependencyGraph graph = new RelationUploadDependencyGraph(relationsToAdd); newToAdd.addAll(graph.computeUploadOrder()); toAdd = newToAdd; } /** * Replies the subset of relations in relations which are not referring to any * new relation * * @param relations a list of relations * @return the subset of relations in relations which are not referring to any * new relation */ protected List filterRelationsNotReferringToNewRelations(Collection relations) { List ret = new LinkedList(); for (Relation relation: relations) { boolean refersToNewRelation = false; for (RelationMember m : relation.getMembers()) { if (m.isRelation() && m.getMember().isNew()) { refersToNewRelation = true; break; } } if (!refersToNewRelation) { ret.add(relation); } } return ret; } /** * Utility class to sort a collection of of new relations with their dependencies * topologically. * */ private class RelationUploadDependencyGraph { private final Logger logger = Logger.getLogger(RelationUploadDependencyGraph.class.getName()); private HashMap> children; private Collection relations; private Set visited; private List uploadOrder; public RelationUploadDependencyGraph() { this.children = new HashMap>(); this.visited = new HashSet(); } public RelationUploadDependencyGraph(Collection relations) { this(); build(relations); } public void build(Collection relations) { this.relations = new HashSet(); for(Relation relation: relations) { if (!relation.isNew() ) { continue; } this.relations.add(relation); for (RelationMember m: relation.getMembers()) { if (m.isRelation() && m.getMember().isNew()) { addDependency(relation, (Relation)m.getMember()); } } } } public Set getChildren(Relation relation) { Set p = children.get(relation); if (p == null) { p = new HashSet(); children.put(relation, p); } return p; } public void addDependency(Relation relation, Relation child) { getChildren(relation).add(child); } protected void visit(Stack path, Relation current) throws CyclicUploadDependencyException{ if (path.contains(current)) { path.push(current); throw new CyclicUploadDependencyException(path); } if (!visited.contains(current)) { path.push(current); visited.add(current); for (Relation dependent : getChildren(current)) { visit(path,dependent); } uploadOrder.add(current); path.pop(); } } public List computeUploadOrder() throws CyclicUploadDependencyException { visited = new HashSet(); uploadOrder = new LinkedList(); Stack path = new Stack(); for (Relation relation: relations) { visit(path, relation); } ArrayList ret = new ArrayList(relations); Collections.sort( ret, new Comparator() { public int compare(Relation o1, Relation o2) { return Integer.valueOf(uploadOrder.indexOf(o1)).compareTo(uploadOrder.indexOf(o2)); } } ); return ret; } } }