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

Last change on this file since 19307 was 19307, checked in by taylor.smock, 11 months ago

Fix most new PMD issues

It would be better to use the newer switch syntax introduced in Java 14 (JEP 361),
but we currently target Java 11+. When we move to Java 17, this should be
reverted and the newer switch syntax should be used.

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