source: josm/trunk/src/org/openstreetmap/josm/command/SplitWayCommand.java@ 17362

Last change on this file since 17362 was 17362, checked in by GerdP, 4 years ago

see #19885: memory leak with "temporary" objects in validator and actions

  • fix memory leaks in SplitWayAction and SplitWayCommand
  • fix (not yet used) counter of modified relations
File size: 42.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITHOUT_DOWNLOADS;
5import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITH_DOWNLOADS;
6import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.USER_ABORTED;
7import static org.openstreetmap.josm.command.SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD;
8import static org.openstreetmap.josm.tools.I18n.tr;
9import static org.openstreetmap.josm.tools.I18n.trn;
10
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.EnumSet;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.Iterator;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Map;
22import java.util.Objects;
23import java.util.Optional;
24import java.util.Set;
25import java.util.function.Consumer;
26import java.util.stream.Collectors;
27
28import javax.swing.JOptionPane;
29
30import org.openstreetmap.josm.data.osm.DataSet;
31import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
32import org.openstreetmap.josm.data.osm.Node;
33import org.openstreetmap.josm.data.osm.OsmPrimitive;
34import org.openstreetmap.josm.data.osm.PrimitiveId;
35import org.openstreetmap.josm.data.osm.Relation;
36import org.openstreetmap.josm.data.osm.RelationMember;
37import org.openstreetmap.josm.data.osm.Way;
38import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
39import org.openstreetmap.josm.gui.ExceptionDialogUtil;
40import org.openstreetmap.josm.gui.MainApplication;
41import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
42import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
43import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
44import org.openstreetmap.josm.io.OsmTransferException;
45import org.openstreetmap.josm.spi.preferences.Config;
46import org.openstreetmap.josm.tools.CheckParameterUtil;
47import org.openstreetmap.josm.tools.Logging;
48/**
49 * Splits a way into multiple ways (all identical except for their node list).
50 *
51 * Ways are just split at the selected nodes. The nodes remain in their
52 * original order. Selected nodes at the end of a way are ignored.
53 *
54 * @since 12828 ({@code SplitWayAction} converted to a {@link Command})
55 */
56public class SplitWayCommand extends SequenceCommand {
57
58 private static volatile Consumer<String> warningNotifier = Logging::warn;
59 private static final String DOWNLOAD_MISSING_PREF_KEY = "split_way_download_missing_members";
60
61 private static final class RelationInformation {
62 boolean warnme;
63 boolean insert;
64 Relation relation;
65 }
66
67 /**
68 * Sets the global warning notifier.
69 * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null
70 */
71 public static void setWarningNotifier(Consumer<String> notifier) {
72 warningNotifier = Objects.requireNonNull(notifier);
73 }
74
75 private final List<? extends PrimitiveId> newSelection;
76 private final Way originalWay;
77 private final List<Way> newWays;
78
79 /** Map&lt;Restriction type, type to treat it as&gt; */
80 private static final Map<String, String> relationSpecialTypes = new HashMap<>();
81 static {
82 relationSpecialTypes.put("restriction", "restriction");
83 relationSpecialTypes.put("destination_sign", "restriction");
84 relationSpecialTypes.put("connectivity", "restriction");
85 }
86
87 /**
88 * Create a new {@code SplitWayCommand}.
89 * @param name The description text
90 * @param commandList The sequence of commands that should be executed.
91 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
92 * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
93 * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getNewWays})
94 */
95 public SplitWayCommand(String name, Collection<Command> commandList,
96 List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
97 super(name, commandList);
98 this.newSelection = newSelection;
99 this.originalWay = originalWay;
100 this.newWays = newWays;
101 }
102
103 /**
104 * Replies the new list of selected primitives ids
105 * @return The new list of selected primitives ids
106 */
107 public List<? extends PrimitiveId> getNewSelection() {
108 return newSelection;
109 }
110
111 /**
112 * Replies the original way being split
113 * @return The original way being split
114 */
115 public Way getOriginalWay() {
116 return originalWay;
117 }
118
119 /**
120 * Replies the resulting new ways
121 * @return The resulting new ways
122 */
123 public List<Way> getNewWays() {
124 return newWays;
125 }
126
127 /**
128 * Determines which way chunk should reuse the old id and its history
129 */
130 @FunctionalInterface
131 public interface Strategy {
132
133 /**
134 * Determines which way chunk should reuse the old id and its history.
135 *
136 * @param wayChunks the way chunks
137 * @return the way to keep
138 */
139 Way determineWayToKeep(Iterable<Way> wayChunks);
140
141 /**
142 * Returns a strategy which selects the way chunk with the highest node count to keep.
143 * @return strategy which selects the way chunk with the highest node count to keep
144 */
145 static Strategy keepLongestChunk() {
146 return wayChunks -> {
147 Way wayToKeep = null;
148 for (Way i : wayChunks) {
149 if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
150 wayToKeep = i;
151 }
152 }
153 return wayToKeep;
154 };
155 }
156
157 /**
158 * Returns a strategy which selects the first way chunk.
159 * @return strategy which selects the first way chunk
160 */
161 static Strategy keepFirstChunk() {
162 return wayChunks -> wayChunks.iterator().next();
163 }
164 }
165
166 /**
167 * Splits the nodes of {@code wayToSplit} into a list of node sequences
168 * which are separated at the nodes in {@code splitPoints}.
169 *
170 * This method displays warning messages if {@code wayToSplit} and/or
171 * {@code splitPoints} aren't consistent.
172 *
173 * Returns null, if building the split chunks fails.
174 *
175 * @param wayToSplit the way to split. Must not be null.
176 * @param splitPoints the nodes where the way is split. Must not be null.
177 * @return the list of chunks
178 */
179 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
180 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
181 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
182
183 Set<Node> nodeSet = new HashSet<>(splitPoints);
184 List<List<Node>> wayChunks = new LinkedList<>();
185 List<Node> currentWayChunk = new ArrayList<>();
186 wayChunks.add(currentWayChunk);
187
188 Iterator<Node> it = wayToSplit.getNodes().iterator();
189 while (it.hasNext()) {
190 Node currentNode = it.next();
191 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
192 currentWayChunk.add(currentNode);
193 if (nodeSet.contains(currentNode) && !atEndOfWay) {
194 currentWayChunk = new ArrayList<>();
195 currentWayChunk.add(currentNode);
196 wayChunks.add(currentWayChunk);
197 }
198 }
199
200 // Handle circular ways specially.
201 // If you split at a circular way at two nodes, you just want to split
202 // it at these points, not also at the former endpoint.
203 // So if the last node is the same first node, join the last and the
204 // first way chunk.
205 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
206 if (wayChunks.size() >= 2
207 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
208 && !nodeSet.contains(wayChunks.get(0).get(0))) {
209 if (wayChunks.size() == 2) {
210 warningNotifier.accept(tr("You must select two or more nodes to split a circular way."));
211 return null;
212 }
213 lastWayChunk.remove(lastWayChunk.size() - 1);
214 lastWayChunk.addAll(wayChunks.get(0));
215 wayChunks.remove(wayChunks.size() - 1);
216 wayChunks.set(0, lastWayChunk);
217 }
218
219 if (wayChunks.size() < 2) {
220 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
221 warningNotifier.accept(
222 tr("You must select two or more nodes to split a circular way."));
223 } else {
224 warningNotifier.accept(
225 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"));
226 }
227 return null;
228 }
229 return wayChunks;
230 }
231
232 /**
233 * Creates new way objects for the way chunks and transfers the keys from the original way.
234 * @param way the original way whose keys are transferred
235 * @param wayChunks the way chunks
236 * @return the new way objects
237 */
238 public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
239 final List<Way> newWays = new ArrayList<>();
240 for (List<Node> wayChunk : wayChunks) {
241 Way wayToAdd = new Way();
242 wayToAdd.setKeys(way.getKeys());
243 wayToAdd.setNodes(wayChunk);
244 newWays.add(wayToAdd);
245 }
246 return newWays;
247 }
248
249 /**
250 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
251 * the result of this process in an instance of {@link SplitWayCommand}.
252 *
253 * Note that changes are not applied to the data yet. You have to
254 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
255 *
256 * @param way the way to split. Must not be null.
257 * @param wayChunks the list of way chunks into the way is split. Must not be null.
258 * @param selection The list of currently selected primitives
259 * @return the result from the split operation
260 */
261 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
262 return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
263 }
264
265 /**
266 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance
267 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should
268 * reuse the old id and its history.
269 * <p>
270 * If the split way is part of relations, and the order of the new parts in these relations cannot be determined due
271 * to missing relation members, the user will be asked to consent to downloading these missing members.
272 * <p>
273 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code
274 * UndoRedoHandler.getInstance().add(result)}.
275 *
276 * @param way the way to split. Must not be null.
277 * @param wayChunks the list of way chunks into the way is split. Must not be null.
278 * @param selection The list of currently selected primitives
279 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
280 * @return the result from the split operation
281 */
282 public static SplitWayCommand splitWay(Way way,
283 List<List<Node>> wayChunks,
284 Collection<? extends OsmPrimitive> selection,
285 Strategy splitStrategy) {
286
287 // This method could be refactored to use an Optional in the future, but would need to be deprecated first
288 // to phase out use by plugins.
289 return splitWay(way, wayChunks, selection, splitStrategy, ASK_USER_FOR_CONSENT_TO_DOWNLOAD).orElse(null);
290 }
291
292 /**
293 * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance
294 * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should
295 * reuse the old id and its history.
296 * <p>
297 * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code
298 * UndoRedoHandler.getInstance().add(result)}.
299 *
300 * @param way the way to split. Must not be null.
301 * @param wayChunks the list of way chunks into the way is split. Must not be null.
302 * @param selection The list of currently selected primitives
303 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its
304 * history
305 * @param whenRelationOrderUncertain What to do when the split way is part of relations, and the order of the new
306 * parts in the relation cannot be determined without downloading missing relation
307 * members.
308 * @return The result from the split operation, may be an empty {@link Optional} if the operation is aborted.
309 */
310 public static Optional<SplitWayCommand> splitWay(Way way,
311 List<List<Node>> wayChunks,
312 Collection<? extends OsmPrimitive> selection,
313 Strategy splitStrategy,
314 WhenRelationOrderUncertain whenRelationOrderUncertain) {
315 // build a list of commands, and also a new selection list
316 final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
317 newSelection.addAll(selection);
318
319 // Create all potential new ways
320 final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
321
322 // Determine which part reuses the existing way
323 final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
324
325 return wayToKeep != null
326 ? doSplitWay(way, wayToKeep, newWays, newSelection, whenRelationOrderUncertain)
327 : Optional.empty();
328 }
329
330 /**
331 * Effectively constructs the {@link SplitWayCommand}.
332 * This method is only public for {@code SplitWayAction}.
333 *
334 * @param way the way to split. Must not be null.
335 * @param wayToKeep way chunk which should reuse the old id and its history
336 * @param newWays potential new ways
337 * @param newSelection new selection list to update (optional: can be null)
338 * @param whenRelationOrderUncertain Action to perform when the order of the new parts in relations the way is
339 * member of could not be reliably determined. See
340 * {@link WhenRelationOrderUncertain}.
341 * @return the {@code SplitWayCommand}
342 */
343 public static Optional<SplitWayCommand> doSplitWay(Way way,
344 Way wayToKeep,
345 List<Way> newWays,
346 List<OsmPrimitive> newSelection,
347 WhenRelationOrderUncertain whenRelationOrderUncertain) {
348 if (whenRelationOrderUncertain == null) whenRelationOrderUncertain = ASK_USER_FOR_CONSENT_TO_DOWNLOAD;
349
350 final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
351 newWays.remove(wayToKeep);
352
353 // Figure out the order of relation members (if any).
354 Analysis analysis = analyseSplit(way, wayToKeep, newWays);
355
356 // If there are relations that cannot be split properly without downloading more members,
357 // present the user with an option to do so, or to abort the split.
358 Set<Relation> relationsNeedingMoreMembers = new HashSet<>();
359 Set<OsmPrimitive> incompleteMembers = new HashSet<>();
360 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
361 if (!relationAnalysis.getNeededIncompleteMembers().isEmpty()) {
362 incompleteMembers.addAll(relationAnalysis.getNeededIncompleteMembers());
363 relationsNeedingMoreMembers.add(relationAnalysis.getRelation());
364 }
365 }
366
367 MissingMemberStrategy missingMemberStrategy;
368 if (relationsNeedingMoreMembers.isEmpty()) {
369 // The split can be performed without any extra downloads.
370 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
371 } else {
372 switch (whenRelationOrderUncertain) {
373 case ASK_USER_FOR_CONSENT_TO_DOWNLOAD:
374 // If the analysis shows that for some relations missing members should be downloaded, offer the user the
375 // chance to consent to this.
376
377 // Only ask the user about downloading missing members when they haven't consented to this before.
378 if (ConditionalOptionPaneUtil.getDialogReturnValue(DOWNLOAD_MISSING_PREF_KEY) == Integer.MAX_VALUE) {
379 // User has previously told us downloading missing relation members is fine.
380 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
381 } else {
382 // Ask the user.
383 missingMemberStrategy = offerToDownloadMissingMembersIfNeeded(analysis, relationsNeedingMoreMembers.size());
384 }
385 break;
386 case SPLIT_ANYWAY:
387 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
388 break;
389 case DOWNLOAD_MISSING_MEMBERS:
390 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
391 break;
392 case ABORT:
393 default:
394 missingMemberStrategy = USER_ABORTED;
395 break;
396 }
397 }
398
399 try {
400 switch (missingMemberStrategy) {
401 case GO_AHEAD_WITH_DOWNLOADS:
402 try {
403 downloadMissingMembers(incompleteMembers);
404 } catch (OsmTransferException e) {
405 ExceptionDialogUtil.explainException(e);
406 return Optional.empty();
407 }
408 // If missing relation members were downloaded, perform the analysis again to find the relation
409 // member order for all relations.
410 analysis.cleanup();
411 analysis = analyseSplit(way, wayToKeep, newWays);
412 break;
413 case GO_AHEAD_WITHOUT_DOWNLOADS:
414 // Proceed with the split with the information we have.
415 // This can mean that there are no missing members we want, or that the user chooses to continue
416 // the split without downloading them.
417 break;
418 case USER_ABORTED:
419 default:
420 return Optional.empty();
421 }
422 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep));
423 } finally {
424 // see #19885
425 wayToKeep.setNodes(null);
426 analysis.cleanup();
427 }
428 }
429
430 static Analysis analyseSplit(Way way,
431 Way wayToKeep,
432 List<Way> newWays) {
433 Collection<Command> commandList = new ArrayList<>();
434 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn",
435 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
436
437 // Change the original way
438 final Way changedWay = new Way(way);
439 changedWay.setNodes(wayToKeep.getNodes());
440 commandList.add(new ChangeNodesCommand(way, changedWay.getNodes()));
441 for (Way wayToAdd : newWays) {
442 commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
443 }
444
445 List<RelationAnalysis> relationAnalyses = new ArrayList<>();
446 EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class);
447 int numberOfRelations = 0;
448
449 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) {
450 if (!r.isUsable()) {
451 continue;
452 }
453
454 boolean isSimpleCase = true;
455
456 Relation c = null;
457 String type = Optional.ofNullable(r.get("type")).orElse("");
458 // Known types of ordered relations.
459 boolean isOrderedRelation = "route".equals(type) || "multipolygon".equals(type) || "boundary".equals(type);
460
461 for (int ir = 0; ir < r.getMembersCount(); ir++) {
462 RelationMember rm = r.getMember(ir);
463 if (rm.getMember() == way) {
464 boolean insert = true;
465 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) {
466 RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWay);
467 if (rValue.warnme) warnings.add(WarningType.GENERIC);
468 insert = rValue.insert;
469 c = rValue.relation;
470 } else if (!isOrderedRelation) {
471 // Warn the user when relations that are not a route or multipolygon are modified as a result
472 // of splitting up the way, because we can't tell if this might break anything.
473 warnings.add(WarningType.GENERIC);
474 }
475 if (c == null) {
476 c = new Relation(r);
477 }
478
479 if (insert) {
480 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
481 warnings.add(WarningType.ROLE);
482 }
483
484 // Attempt to determine the direction the ways in the relation are ordered.
485 Direction direction = Direction.UNKNOWN;
486 Set<Way> missingWays = new HashSet<>();
487 if (isOrderedRelation) {
488 if (way.lastNode() == way.firstNode()) {
489 // Self-closing way.
490 direction = Direction.IRRELEVANT;
491 } else {
492 // For ordered relations, looking beyond the nearest neighbour members is not required,
493 // and can even cause the wrong direction to be guessed (with closed loops).
494 if (ir - 1 >= 0 && r.getMember(ir - 1).isWay()) {
495 Way w = r.getMember(ir - 1).getWay();
496 if (w.isIncomplete())
497 missingWays.add(w);
498 else {
499 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
500 direction = Direction.FORWARDS;
501 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
502 direction = Direction.BACKWARDS;
503 }
504 }
505 }
506 if (ir + 1 < r.getMembersCount() && r.getMember(ir + 1).isWay()) {
507 Way w = r.getMember(ir + 1).getWay();
508 if (w.isIncomplete())
509 missingWays.add(w);
510 else {
511 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
512 direction = Direction.BACKWARDS;
513 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
514 direction = Direction.FORWARDS;
515 }
516 }
517 }
518
519 if (direction == Direction.UNKNOWN && missingWays.isEmpty()) {
520 // we cannot detect the direction and no way is missing.
521 // We can safely assume that the direction doesn't matter.
522 direction = Direction.IRRELEVANT;
523 }
524 }
525 } else {
526 int k = 1;
527 while (ir - k >= 0 || ir + k < r.getMembersCount()) {
528 if (ir - k >= 0 && r.getMember(ir - k).isWay()) {
529 Way w = r.getMember(ir - k).getWay();
530 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
531 direction = Direction.FORWARDS;
532 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
533 direction = Direction.BACKWARDS;
534 }
535 break;
536 }
537 if (ir + k < r.getMembersCount() && r.getMember(ir + k).isWay()) {
538 Way w = r.getMember(ir + k).getWay();
539 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
540 direction = Direction.BACKWARDS;
541 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
542 direction = Direction.FORWARDS;
543 }
544 break;
545 }
546 k++;
547 }
548 }
549
550 if (direction == Direction.UNKNOWN) {
551 // We don't have enough information to determine the order of the new ways in this relation.
552 // This may cause relations to be saved with the two new way sections in reverse order.
553 //
554 // This often breaks routes.
555 //
556 } else {
557 missingWays = Collections.emptySet();
558 }
559 relationAnalyses.add(new RelationAnalysis(c, rm, direction, missingWays));
560 isSimpleCase = false;
561 }
562 }
563 }
564 if (!isSimpleCase)
565 numberOfRelations++;
566 if (c != null && isSimpleCase) {
567 if (!r.getMembers().equals(c.getMembers())) {
568 commandList.add(new ChangeMembersCommand(r, new ArrayList<>(c.getMembers())));
569 numberOfRelations++;
570 }
571 c.setMembers(null); // see #19885
572 }
573 }
574 changedWay.setNodes(null); // see #19885
575 return new Analysis(relationAnalyses, commandList, warnings, numberOfRelations);
576 }
577
578 static class Analysis {
579 List<RelationAnalysis> relationAnalyses;
580 Collection<Command> commands;
581 EnumSet<WarningType> warningTypes;
582 private final int numberOfRelations;
583
584 Analysis(List<RelationAnalysis> relationAnalyses,
585 Collection<Command> commandList,
586 EnumSet<WarningType> warnings,
587 int numberOfRelations) {
588 this.relationAnalyses = relationAnalyses;
589 commands = commandList;
590 warningTypes = warnings;
591 this.numberOfRelations = numberOfRelations;
592 }
593
594 /**
595 * Unlink temporary copies of relations. See #19885
596 */
597 void cleanup() {
598 for (RelationAnalysis ra : relationAnalyses) {
599 if (ra.relation.getDataSet() == null)
600 ra.relation.setMembers(null);
601 }
602 }
603
604 List<RelationAnalysis> getRelationAnalyses() {
605 return relationAnalyses;
606 }
607
608 Collection<Command> getCommands() {
609 return commands;
610 }
611
612 EnumSet<WarningType> getWarningTypes() {
613 return warningTypes;
614 }
615
616 public int getNumberOfRelations() {
617 return numberOfRelations;
618 }
619 }
620
621 static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis,
622 int numRelationsNeedingMoreMembers) {
623 String[] options = {
624 tr("Yes, download the missing members"),
625 tr("No, abort the split operation"),
626 tr("No, perform the split without downloading")
627 };
628
629 String msgMemberOfRelations = trn(
630 "This way is part of a relation.",
631 "This way is part of {0} relations.",
632 analysis.getNumberOfRelations(),
633 analysis.getNumberOfRelations()
634 );
635
636 String msgReferToRelations;
637 if (analysis.getNumberOfRelations() == 1) {
638 msgReferToRelations = tr("this relation");
639 } else if (analysis.getNumberOfRelations() == numRelationsNeedingMoreMembers) {
640 msgReferToRelations = tr("these relations");
641 } else {
642 msgReferToRelations = trn(
643 "one relation",
644 "{0} relations",
645 numRelationsNeedingMoreMembers,
646 numRelationsNeedingMoreMembers
647 );
648 }
649
650 String msgRelationsMissingData = tr(
651 "For {0} the correct order of the new way parts could not be determined. " +
652 "To fix this, some missing relation members should be downloaded first.",
653 msgReferToRelations
654 );
655
656 JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData);
657 msg.setMaxWidth(600);
658
659 int ret = JOptionPane.showOptionDialog(
660 MainApplication.getMainFrame(),
661 msg,
662 tr("Download missing relation members?"),
663 JOptionPane.OK_CANCEL_OPTION,
664 JOptionPane.QUESTION_MESSAGE,
665 null,
666 options,
667 options[0]
668 );
669
670 switch (ret) {
671 case JOptionPane.OK_OPTION:
672 // Ask the user if they want to do this automatically from now on. We only ask this for the download
673 // action, because automatically cancelling is confusing (the user can't tell why this happened), and
674 // automatically performing the split without downloading missing members despite needing them is
675 // likely to break a lot of routes. The user also can't tell the difference between a split that needs
676 // no downloads at all, and this special case where downloading missing relation members will prevent
677 // broken relations.
678 ConditionalOptionPaneUtil.showMessageDialog(
679 DOWNLOAD_MISSING_PREF_KEY,
680 MainApplication.getMainFrame(),
681 tr("Missing relation members will be downloaded. Should this be done automatically from now on?"),
682 tr("Downloading missing relation members"),
683 JOptionPane.INFORMATION_MESSAGE
684 );
685 return GO_AHEAD_WITH_DOWNLOADS;
686 case JOptionPane.CANCEL_OPTION:
687 return GO_AHEAD_WITHOUT_DOWNLOADS;
688 default:
689 return USER_ABORTED;
690 }
691 }
692
693 static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException {
694 // Download the missing members.
695 MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create();
696 reader.append(incompleteMembers);
697
698 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
699 MainApplication.getLayerManager().getEditLayer().mergeFrom(ds);
700 }
701
702 static SplitWayCommand splitBasedOnAnalyses(Way way,
703 List<Way> newWays,
704 List<OsmPrimitive> newSelection,
705 Analysis analysis,
706 int indexOfWayToKeep) {
707 if (newSelection != null && !newSelection.contains(way)) {
708 newSelection.add(way);
709 }
710
711 if (newSelection != null) {
712 newSelection.addAll(newWays);
713 }
714
715 // Perform the split.
716 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
717 RelationMember rm = relationAnalysis.getRelationMember();
718 Relation relation = relationAnalysis.getRelation();
719 Direction direction = relationAnalysis.getDirection();
720
721 int position = -1;
722 for (int i = 0; i < relation.getMembersCount(); i++) {
723 // search for identical member (can't use indexOf() as it uses equals()
724 if (rm == relation.getMember(i)) {
725 position = i;
726 break;
727 }
728 }
729
730 // sanity check
731 if (position < 0) {
732 throw new AssertionError("Relation member not found");
733 }
734
735 int j = position;
736 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
737 for (Way wayToAdd : waysToAddBefore) {
738 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
739 j++;
740 if (direction == Direction.BACKWARDS) {
741 relation.addMember(position + 1, em);
742 } else {
743 relation.addMember(j - 1, em);
744 }
745 }
746 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
747 for (Way wayToAdd : waysToAddAfter) {
748 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
749 j++;
750 if (direction == Direction.BACKWARDS) {
751 relation.addMember(position, em);
752 } else {
753 relation.addMember(j, em);
754 }
755 }
756 }
757
758 // add one command for each complex case with relations
759 final DataSet ds = way.getDataSet();
760 for (Relation r : analysis.getRelationAnalyses().stream().map(RelationAnalysis::getRelation).collect(Collectors.toSet())) {
761 Relation orig = (Relation) ds.getPrimitiveById(r);
762 analysis.getCommands().add(new ChangeMembersCommand(orig, new ArrayList<>(r.getMembers())));
763 r.setMembers(null); // see #19885
764 }
765
766 EnumSet<WarningType> warnings = analysis.getWarningTypes();
767
768 if (warnings.contains(WarningType.ROLE)) {
769 warningNotifier.accept(
770 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
771 } else if (warnings.contains(WarningType.GENERIC)) {
772 warningNotifier.accept(
773 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
774 }
775
776 return new SplitWayCommand(
777 /* for correct i18n of plural forms - see #9110 */
778 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
779 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
780 analysis.getCommands(),
781 newSelection,
782 way,
783 newWays
784 );
785 }
786
787 private static RelationInformation treatAsRestriction(Relation r,
788 RelationMember rm, Relation c, Collection<Way> newWays, Way way,
789 Way changedWay) {
790 RelationInformation relationInformation = new RelationInformation();
791 /* this code assumes the restriction is correct. No real error checking done */
792 String role = rm.getRole();
793 String type = Optional.ofNullable(r.get("type")).orElse("");
794 if ("from".equals(role) || "to".equals(role)) {
795 List<Node> nodes = new ArrayList<>();
796 for (OsmPrimitive via : findVias(r, type)) {
797 if (via instanceof Node) {
798 nodes.add((Node) via);
799 } else if (via instanceof Way) {
800 nodes.add(((Way) via).lastNode());
801 nodes.add(((Way) via).firstNode());
802 }
803 }
804 Way res = null;
805 for (Node n : nodes) {
806 if (changedWay.isFirstLastNode(n)) {
807 res = way;
808 }
809 }
810 if (res == null) {
811 for (Way wayToAdd : newWays) {
812 for (Node n : nodes) {
813 if (wayToAdd.isFirstLastNode(n)) {
814 res = wayToAdd;
815 }
816 }
817 }
818 if (res != null) {
819 if (c == null) {
820 c = new Relation(r);
821 }
822 c.addMember(new RelationMember(role, res));
823 c.removeMembersFor(way);
824 relationInformation.insert = false;
825 }
826 } else {
827 relationInformation.insert = false;
828 }
829 } else if (!"via".equals(role)) {
830 relationInformation.warnme = true;
831 }
832 relationInformation.relation = c;
833 return relationInformation;
834 }
835
836 static List<? extends OsmPrimitive> findVias(Relation r, String type) {
837 if (type != null) {
838 switch (type) {
839 case "connectivity":
840 case "restriction":
841 return r.findRelationMembers("via");
842 case "destination_sign":
843 // Prefer intersection over sign, see #12347
844 List<? extends OsmPrimitive> intersections = r.findRelationMembers("intersection");
845 return intersections.isEmpty() ? r.findRelationMembers("sign") : intersections;
846 default:
847 break;
848 }
849 }
850 return Collections.emptyList();
851 }
852
853 /**
854 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
855 * the result of this process in an instance of {@link SplitWayCommand}.
856 *
857 * Note that changes are not applied to the data yet. You have to
858 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
859 *
860 * Replies null if the way couldn't be split at the given nodes.
861 *
862 * @param way the way to split. Must not be null.
863 * @param atNodes the list of nodes where the way is split. Must not be null.
864 * @param selection The list of currently selected primitives
865 * @return the result from the split operation
866 */
867 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
868 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
869 return chunks != null ? splitWay(way, chunks, selection) : null;
870 }
871
872 /**
873 * Add relations that are treated in a specific way.
874 * @param relationType The value in the {@code type} key
875 * @param treatAs The type of relation to treat the {@code relationType} as.
876 * Currently only supports relations that can be handled like "restriction"
877 * relations.
878 * @return the previous value associated with relationType, or null if there was no mapping
879 * @since 15078
880 */
881 public static String addSpecialRelationType(String relationType, String treatAs) {
882 return relationSpecialTypes.put(relationType, treatAs);
883 }
884
885 /**
886 * Get the types of relations that are treated differently
887 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>}
888 * @since 15078
889 */
890 public static Map<String, String> getSpecialRelationTypes() {
891 return relationSpecialTypes;
892 }
893
894 /**
895 * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be
896 * determined without downloading missing relation members.
897 */
898 public enum WhenRelationOrderUncertain {
899 /**
900 * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to
901 * proceed without downloading anything.
902 */
903 ASK_USER_FOR_CONSENT_TO_DOWNLOAD,
904 /**
905 * If there are relation members missing, and these are needed to determine the order of the new parts in
906 * that relation, abort the split operation.
907 */
908 ABORT,
909 /**
910 * If there are relation members missing, and these are needed to determine the order of the new parts in
911 * that relation, continue with the split operation anyway, without downloading anything. Caution: use this
912 * option with care.
913 */
914 SPLIT_ANYWAY,
915 /**
916 * If there are relation members missing, and these are needed to determine the order of the new parts in
917 * that relation, automatically download these without prompting the user.
918 */
919 DOWNLOAD_MISSING_MEMBERS
920 }
921
922 static class RelationAnalysis {
923 private final Relation relation;
924 private final RelationMember relationMember;
925 private final Direction direction;
926 private final Set<Way> neededIncompleteMembers;
927
928 RelationAnalysis(Relation relation,
929 RelationMember relationMember,
930 Direction direction,
931 Set<Way> neededIncompleteMembers) {
932 this.relation = relation;
933 this.relationMember = relationMember;
934 this.direction = direction;
935 this.neededIncompleteMembers = neededIncompleteMembers;
936 }
937
938 RelationMember getRelationMember() {
939 return relationMember;
940 }
941
942 Direction getDirection() {
943 return direction;
944 }
945
946 public Set<Way> getNeededIncompleteMembers() {
947 return neededIncompleteMembers;
948 }
949
950 Relation getRelation() {
951 return relation;
952 }
953 }
954
955 enum Direction {
956 FORWARDS,
957 BACKWARDS,
958 UNKNOWN,
959 IRRELEVANT
960 }
961
962 enum WarningType {
963 GENERIC,
964 ROLE
965 }
966
967 enum MissingMemberStrategy {
968 GO_AHEAD_WITH_DOWNLOADS,
969 GO_AHEAD_WITHOUT_DOWNLOADS,
970 USER_ABORTED
971 }
972}
Note: See TracBrowser for help on using the repository browser.