source: josm/trunk/src/org/openstreetmap/josm/data/APIDataSet.java @ 13161

Last change on this file since 13161 was 13161, checked in by Don-vip, 15 months ago

fix #13153 - Should not warn to upload/save "modified" layers with 0 objects

  • Property svn:eol-style set to native
File size: 12.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.Comparator;
7import java.util.HashMap;
8import java.util.HashSet;
9import java.util.LinkedList;
10import java.util.List;
11import java.util.Map;
12import java.util.Set;
13import java.util.Stack;
14import java.util.stream.Collectors;
15import java.util.stream.Stream;
16
17import org.openstreetmap.josm.data.conflict.ConflictCollection;
18import org.openstreetmap.josm.data.osm.CyclicUploadDependencyException;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.data.osm.IPrimitive;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
24import org.openstreetmap.josm.data.osm.PrimitiveId;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.RelationMember;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.tools.Logging;
29import org.openstreetmap.josm.tools.Utils;
30
31/**
32 * Represents a collection of {@link OsmPrimitive}s which should be uploaded to the API.
33 * The collection is derived from the modified primitives of an {@link DataSet} and it provides methods
34 * for sorting the objects in upload order.
35 * @since 2025
36 */
37public class APIDataSet {
38    private List<OsmPrimitive> toAdd;
39    private List<OsmPrimitive> toUpdate;
40    private List<OsmPrimitive> toDelete;
41
42    /**
43     * The type of operation we can perform with OSM API on a primitive.
44     * @since 13161
45     */
46    public enum APIOperation {
47        /** Add a new primitive */
48        ADD,
49        /** Update an existing primitive */
50        UPDATE,
51        /** Delete an existing primitive */
52        DELETE;
53
54        /**
55         * Determines the API operation to perform on a primitive.
56         * @param osm OSM primitive
57         * @return the API operation to perform on {@code osm}
58         */
59        public static APIOperation of(OsmPrimitive osm) {
60            if (osm.isNewOrUndeleted() && !osm.isDeleted()) {
61                return ADD;
62            } else if (osm.isModified() && !osm.isDeleted()) {
63                return UPDATE;
64            } else if (osm.isDeleted() && !osm.isNew() && osm.isModified() && osm.isVisible()) {
65                return DELETE;
66            }
67            return null;
68        }
69    }
70
71    /**
72     * creates a new empty data set
73     */
74    public APIDataSet() {
75        toAdd = new LinkedList<>();
76        toUpdate = new LinkedList<>();
77        toDelete = new LinkedList<>();
78    }
79
80    /**
81     * initializes the API data set with the modified primitives in <code>ds</code>
82     *
83     * @param ds the data set. Ignored, if null.
84     */
85    public void init(DataSet ds) {
86        if (ds == null) return;
87        init(ds.allPrimitives());
88    }
89
90    /**
91     * Initializes the API data set with the modified primitives, ignores unmodified primitives.
92     *
93     * @param primitives the primitives
94     */
95    public final void init(Collection<OsmPrimitive> primitives) {
96        toAdd.clear();
97        toUpdate.clear();
98        toDelete.clear();
99
100        for (OsmPrimitive osm :primitives) {
101            switch (APIOperation.of(osm)) {
102                case ADD: toAdd.add(osm); break;
103                case UPDATE: toUpdate.add(osm); break;
104                case DELETE: toDelete.add(osm); break;
105                default: Logging.trace("Ignored primitive {0}", osm);
106            }
107        }
108        final Comparator<OsmPrimitive> orderingNodesWaysRelations = OsmPrimitiveComparator.orderingNodesWaysRelations();
109        final Comparator<OsmPrimitive> byUniqueId = OsmPrimitiveComparator.comparingUniqueId();
110        toAdd.sort(orderingNodesWaysRelations.thenComparing(byUniqueId));
111        toUpdate.sort(orderingNodesWaysRelations.thenComparing(byUniqueId));
112        toDelete.sort(orderingNodesWaysRelations.reversed().thenComparing(byUniqueId));
113    }
114
115    /**
116     * initializes the API data set with the modified primitives in <code>ds</code>
117     *
118     * @param ds the data set. Ignored, if null.
119     */
120    public APIDataSet(DataSet ds) {
121        this();
122        init(ds);
123    }
124
125    /**
126     * Replies true if one of the primitives to be updated or to be deleted
127     * participates in at least one conflict in <code>conflicts</code>
128     *
129     * @param conflicts the collection of conflicts
130     * @return true if one of the primitives to be updated or to be deleted
131     * participates in at least one conflict in <code>conflicts</code>
132     */
133    public boolean participatesInConflict(ConflictCollection conflicts) {
134        if (conflicts == null || conflicts.isEmpty()) return false;
135        Set<PrimitiveId> idsParticipatingInConflicts = conflicts.get().stream()
136                .flatMap(c -> Stream.of(c.getMy(), c.getTheir()))
137                .map(OsmPrimitive::getPrimitiveId)
138                .collect(Collectors.toSet());
139        return Stream.of(toUpdate, toDelete)
140                .flatMap(Collection::stream)
141                .map(OsmPrimitive::getPrimitiveId)
142                .anyMatch(idsParticipatingInConflicts::contains);
143    }
144
145    /**
146     * initializes the API data set with the primitives in <code>primitives</code>
147     *
148     * @param primitives the collection of primitives
149     */
150    public APIDataSet(Collection<OsmPrimitive> primitives) {
151        this();
152        init(primitives);
153    }
154
155    /**
156     * Replies true if there are no primitives to upload
157     *
158     * @return true if there are no primitives to upload
159     */
160    public boolean isEmpty() {
161        return toAdd.isEmpty() && toUpdate.isEmpty() && toDelete.isEmpty();
162    }
163
164    /**
165     * Replies the primitives which should be added to the OSM database
166     *
167     * @return the primitives which should be added to the OSM database
168     */
169    public List<OsmPrimitive> getPrimitivesToAdd() {
170        return toAdd;
171    }
172
173    /**
174     * Replies the primitives which should be updated in the OSM database
175     *
176     * @return the primitives which should be updated in the OSM database
177     */
178    public List<OsmPrimitive> getPrimitivesToUpdate() {
179        return toUpdate;
180    }
181
182    /**
183     * Replies the primitives which should be deleted in the OSM database
184     *
185     * @return the primitives which should be deleted in the OSM database
186     */
187    public List<OsmPrimitive> getPrimitivesToDelete() {
188        return toDelete;
189    }
190
191    /**
192     * Replies all primitives
193     *
194     * @return all primitives
195     */
196    public List<OsmPrimitive> getPrimitives() {
197        List<OsmPrimitive> ret = new LinkedList<>();
198        ret.addAll(toAdd);
199        ret.addAll(toUpdate);
200        ret.addAll(toDelete);
201        return ret;
202    }
203
204    /**
205     * Replies the number of objects to upload
206     *
207     * @return the number of objects to upload
208     */
209    public int getSize() {
210        return toAdd.size() + toUpdate.size() + toDelete.size();
211    }
212
213    /**
214     * Removes the given primitives from this {@link APIDataSet}
215     * @param processed The primitives to remove
216     */
217    public void removeProcessed(Collection<IPrimitive> processed) {
218        if (processed == null) return;
219        toAdd.removeAll(processed);
220        toUpdate.removeAll(processed);
221        toDelete.removeAll(processed);
222    }
223
224    /**
225     * Adjusts the upload order for new relations. Child relations are uploaded first,
226     * parent relations second.
227     *
228     * This method detects cyclic dependencies in new relation. Relations with cyclic
229     * dependencies can't be uploaded.
230     *
231     * @throws CyclicUploadDependencyException if a cyclic dependency is detected
232     */
233    public void adjustRelationUploadOrder() throws CyclicUploadDependencyException {
234        List<OsmPrimitive> newToAdd = new LinkedList<>();
235        newToAdd.addAll(Utils.filteredCollection(toAdd, Node.class));
236        newToAdd.addAll(Utils.filteredCollection(toAdd, Way.class));
237
238        List<Relation> relationsToAdd = new ArrayList<>(Utils.filteredCollection(toAdd, Relation.class));
239        List<Relation> noProblemRelations = filterRelationsNotReferringToNewRelations(relationsToAdd);
240        newToAdd.addAll(noProblemRelations);
241        relationsToAdd.removeAll(noProblemRelations);
242
243        RelationUploadDependencyGraph graph = new RelationUploadDependencyGraph(relationsToAdd, true);
244        newToAdd.addAll(graph.computeUploadOrder(false));
245        toAdd = newToAdd;
246
247        List<OsmPrimitive> newToDelete = new LinkedList<>();
248        graph = new RelationUploadDependencyGraph(Utils.filteredCollection(toDelete, Relation.class), false);
249        newToDelete.addAll(graph.computeUploadOrder(true));
250        newToDelete.addAll(Utils.filteredCollection(toDelete, Way.class));
251        newToDelete.addAll(Utils.filteredCollection(toDelete, Node.class));
252        toDelete = newToDelete;
253    }
254
255    /**
256     * Replies the subset of relations in <code>relations</code> which are not referring to any
257     * new relation
258     *
259     * @param relations a list of relations
260     * @return the subset of relations in <code>relations</code> which are not referring to any
261     * new relation
262     */
263    protected List<Relation> filterRelationsNotReferringToNewRelations(Collection<Relation> relations) {
264        List<Relation> ret = new LinkedList<>();
265        for (Relation relation: relations) {
266            boolean refersToNewRelation = false;
267            for (RelationMember m : relation.getMembers()) {
268                if (m.isRelation() && m.getMember().isNewOrUndeleted()) {
269                    refersToNewRelation = true;
270                    break;
271                }
272            }
273            if (!refersToNewRelation) {
274                ret.add(relation);
275            }
276        }
277        return ret;
278    }
279
280    /**
281     * Utility class to sort a collection of new relations with their dependencies
282     * topologically.
283     *
284     */
285    private static class RelationUploadDependencyGraph {
286        private final Map<Relation, Set<Relation>> children = new HashMap<>();
287        private Collection<Relation> relations;
288        private Set<Relation> visited = new HashSet<>();
289        private List<Relation> uploadOrder;
290        private final boolean newOrUndeleted;
291
292        RelationUploadDependencyGraph(Collection<Relation> relations, boolean newOrUndeleted) {
293            this.newOrUndeleted = newOrUndeleted;
294            build(relations);
295        }
296
297        public final void build(Collection<Relation> relations) {
298            this.relations = new HashSet<>();
299            for (Relation relation: relations) {
300                if (newOrUndeleted ? !relation.isNewOrUndeleted() : !relation.isDeleted()) {
301                    continue;
302                }
303                this.relations.add(relation);
304                for (RelationMember m: relation.getMembers()) {
305                    if (m.isRelation() && (newOrUndeleted ? m.getMember().isNewOrUndeleted() : m.getMember().isDeleted())) {
306                        addDependency(relation, (Relation) m.getMember());
307                    }
308                }
309            }
310        }
311
312        public Set<Relation> getChildren(Relation relation) {
313            return children.computeIfAbsent(relation, k -> new HashSet<>());
314        }
315
316        public void addDependency(Relation relation, Relation child) {
317            getChildren(relation).add(child);
318        }
319
320        protected void visit(Stack<Relation> path, Relation current) throws CyclicUploadDependencyException {
321            if (path.contains(current)) {
322                path.push(current);
323                throw new CyclicUploadDependencyException(path);
324            }
325            if (!visited.contains(current)) {
326                path.push(current);
327                visited.add(current);
328                for (Relation dependent : getChildren(current)) {
329                    visit(path, dependent);
330                }
331                uploadOrder.add(current);
332                path.pop();
333            }
334        }
335
336        public List<Relation> computeUploadOrder(boolean reverse) throws CyclicUploadDependencyException {
337            visited = new HashSet<>();
338            uploadOrder = new LinkedList<>();
339            Stack<Relation> path = new Stack<>();
340            for (Relation relation: relations) {
341                visit(path, relation);
342            }
343            List<Relation> ret = new ArrayList<>(relations);
344            Comparator<? super Relation> cmpr = Comparator.comparingInt(uploadOrder::indexOf);
345            if (reverse) {
346                cmpr = cmpr.reversed();
347            }
348            ret.sort(cmpr);
349            return ret;
350        }
351    }
352}
Note: See TracBrowser for help on using the repository browser.