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

Last change on this file since 17367 was 17367, checked in by GerdP, 3 years ago

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

  • simplify code in SplitWayCommand, no need to create a way when we only need to know the nodes
File size: 42.8 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 List<Node> changedWayNodes = wayToKeep.getNodes();
439 commandList.add(new ChangeNodesCommand(way, changedWayNodes));
440 for (Way wayToAdd : newWays) {
441 commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
442 }
443
444 List<RelationAnalysis> relationAnalyses = new ArrayList<>();
445 EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class);
446 int numberOfRelations = 0;
447
448 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) {
449 if (!r.isUsable()) {
450 continue;
451 }
452
453 numberOfRelations++;
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, changedWayNodes);
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 (c != null && isSimpleCase) {
565 if (!r.getMembers().equals(c.getMembers())) {
566 commandList.add(new ChangeMembersCommand(r, new ArrayList<>(c.getMembers())));
567 }
568 c.setMembers(null); // see #19885
569 }
570 }
571 return new Analysis(relationAnalyses, commandList, warnings, numberOfRelations);
572 }
573
574 static class Analysis {
575 List<RelationAnalysis> relationAnalyses;
576 Collection<Command> commands;
577 EnumSet<WarningType> warningTypes;
578 private final int numberOfRelations;
579
580 Analysis(List<RelationAnalysis> relationAnalyses,
581 Collection<Command> commandList,
582 EnumSet<WarningType> warnings,
583 int numberOfRelations) {
584 this.relationAnalyses = relationAnalyses;
585 commands = commandList;
586 warningTypes = warnings;
587 this.numberOfRelations = numberOfRelations;
588 }
589
590 /**
591 * Unlink temporary copies of relations. See #19885
592 */
593 void cleanup() {
594 for (RelationAnalysis ra : relationAnalyses) {
595 if (ra.relation.getDataSet() == null)
596 ra.relation.setMembers(null);
597 }
598 }
599
600 List<RelationAnalysis> getRelationAnalyses() {
601 return relationAnalyses;
602 }
603
604 Collection<Command> getCommands() {
605 return commands;
606 }
607
608 EnumSet<WarningType> getWarningTypes() {
609 return warningTypes;
610 }
611
612 public int getNumberOfRelations() {
613 return numberOfRelations;
614 }
615 }
616
617 static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis,
618 int numRelationsNeedingMoreMembers) {
619 String[] options = {
620 tr("Yes, download the missing members"),
621 tr("No, abort the split operation"),
622 tr("No, perform the split without downloading")
623 };
624
625 String msgMemberOfRelations = trn(
626 "This way is part of a relation.",
627 "This way is part of {0} relations.",
628 analysis.getNumberOfRelations(),
629 analysis.getNumberOfRelations()
630 );
631
632 String msgReferToRelations;
633 if (analysis.getNumberOfRelations() == 1) {
634 msgReferToRelations = tr("this relation");
635 } else if (analysis.getNumberOfRelations() == numRelationsNeedingMoreMembers) {
636 msgReferToRelations = tr("these relations");
637 } else {
638 msgReferToRelations = trn(
639 "one relation",
640 "{0} relations",
641 numRelationsNeedingMoreMembers,
642 numRelationsNeedingMoreMembers
643 );
644 }
645
646 String msgRelationsMissingData = tr(
647 "For {0} the correct order of the new way parts could not be determined. " +
648 "To fix this, some missing relation members should be downloaded first.",
649 msgReferToRelations
650 );
651
652 JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData);
653 msg.setMaxWidth(600);
654
655 int ret = JOptionPane.showOptionDialog(
656 MainApplication.getMainFrame(),
657 msg,
658 tr("Download missing relation members?"),
659 JOptionPane.OK_CANCEL_OPTION,
660 JOptionPane.QUESTION_MESSAGE,
661 null,
662 options,
663 options[0]
664 );
665
666 switch (ret) {
667 case JOptionPane.OK_OPTION:
668 // Ask the user if they want to do this automatically from now on. We only ask this for the download
669 // action, because automatically cancelling is confusing (the user can't tell why this happened), and
670 // automatically performing the split without downloading missing members despite needing them is
671 // likely to break a lot of routes. The user also can't tell the difference between a split that needs
672 // no downloads at all, and this special case where downloading missing relation members will prevent
673 // broken relations.
674 ConditionalOptionPaneUtil.showMessageDialog(
675 DOWNLOAD_MISSING_PREF_KEY,
676 MainApplication.getMainFrame(),
677 tr("Missing relation members will be downloaded. Should this be done automatically from now on?"),
678 tr("Downloading missing relation members"),
679 JOptionPane.INFORMATION_MESSAGE
680 );
681 return GO_AHEAD_WITH_DOWNLOADS;
682 case JOptionPane.CANCEL_OPTION:
683 return GO_AHEAD_WITHOUT_DOWNLOADS;
684 default:
685 return USER_ABORTED;
686 }
687 }
688
689 static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException {
690 // Download the missing members.
691 MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create();
692 reader.append(incompleteMembers);
693
694 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
695 MainApplication.getLayerManager().getEditLayer().mergeFrom(ds);
696 }
697
698 static SplitWayCommand splitBasedOnAnalyses(Way way,
699 List<Way> newWays,
700 List<OsmPrimitive> newSelection,
701 Analysis analysis,
702 int indexOfWayToKeep) {
703 if (newSelection != null && !newSelection.contains(way)) {
704 newSelection.add(way);
705 }
706
707 if (newSelection != null) {
708 newSelection.addAll(newWays);
709 }
710
711 // Perform the split.
712 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
713 RelationMember rm = relationAnalysis.getRelationMember();
714 Relation relation = relationAnalysis.getRelation();
715 Direction direction = relationAnalysis.getDirection();
716
717 int position = -1;
718 for (int i = 0; i < relation.getMembersCount(); i++) {
719 // search for identical member (can't use indexOf() as it uses equals()
720 if (rm == relation.getMember(i)) {
721 position = i;
722 break;
723 }
724 }
725
726 // sanity check
727 if (position < 0) {
728 throw new AssertionError("Relation member not found");
729 }
730
731 int j = position;
732 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
733 for (Way wayToAdd : waysToAddBefore) {
734 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
735 j++;
736 if (direction == Direction.BACKWARDS) {
737 relation.addMember(position + 1, em);
738 } else {
739 relation.addMember(j - 1, em);
740 }
741 }
742 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
743 for (Way wayToAdd : waysToAddAfter) {
744 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
745 j++;
746 if (direction == Direction.BACKWARDS) {
747 relation.addMember(position, em);
748 } else {
749 relation.addMember(j, em);
750 }
751 }
752 }
753
754 // add one command for each complex case with relations
755 final DataSet ds = way.getDataSet();
756 for (Relation r : analysis.getRelationAnalyses().stream().map(RelationAnalysis::getRelation).collect(Collectors.toSet())) {
757 Relation orig = (Relation) ds.getPrimitiveById(r);
758 analysis.getCommands().add(new ChangeMembersCommand(orig, new ArrayList<>(r.getMembers())));
759 r.setMembers(null); // see #19885
760 }
761
762 EnumSet<WarningType> warnings = analysis.getWarningTypes();
763
764 if (warnings.contains(WarningType.ROLE)) {
765 warningNotifier.accept(
766 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
767 } else if (warnings.contains(WarningType.GENERIC)) {
768 warningNotifier.accept(
769 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
770 }
771
772 return new SplitWayCommand(
773 /* for correct i18n of plural forms - see #9110 */
774 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
775 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
776 analysis.getCommands(),
777 newSelection,
778 way,
779 newWays
780 );
781 }
782
783 private static RelationInformation treatAsRestriction(Relation r,
784 RelationMember rm, Relation c, Collection<Way> newWays, Way way,
785 List<Node> changedWayNodes) {
786 RelationInformation relationInformation = new RelationInformation();
787 /* this code assumes the restriction is correct. No real error checking done */
788 String role = rm.getRole();
789 String type = Optional.ofNullable(r.get("type")).orElse("");
790 if ("from".equals(role) || "to".equals(role)) {
791 List<Node> nodes = new ArrayList<>();
792 for (OsmPrimitive via : findVias(r, type)) {
793 if (via instanceof Node) {
794 nodes.add((Node) via);
795 } else if (via instanceof Way) {
796 nodes.add(((Way) via).lastNode());
797 nodes.add(((Way) via).firstNode());
798 }
799 }
800 Way res = null;
801 for (Node n : nodes) {
802 if (changedWayNodes.get(0) == n || changedWayNodes.get(changedWayNodes.size() - 1) == n) {
803 res = way;
804 }
805 }
806 if (res == null) {
807 for (Way wayToAdd : newWays) {
808 for (Node n : nodes) {
809 if (wayToAdd.isFirstLastNode(n)) {
810 res = wayToAdd;
811 }
812 }
813 }
814 if (res != null) {
815 if (c == null) {
816 c = new Relation(r);
817 }
818 c.addMember(new RelationMember(role, res));
819 c.removeMembersFor(way);
820 relationInformation.insert = false;
821 }
822 } else {
823 relationInformation.insert = false;
824 }
825 } else if (!"via".equals(role)) {
826 relationInformation.warnme = true;
827 }
828 relationInformation.relation = c;
829 return relationInformation;
830 }
831
832 static List<? extends OsmPrimitive> findVias(Relation r, String type) {
833 if (type != null) {
834 switch (type) {
835 case "connectivity":
836 case "restriction":
837 return r.findRelationMembers("via");
838 case "destination_sign":
839 // Prefer intersection over sign, see #12347
840 List<? extends OsmPrimitive> intersections = r.findRelationMembers("intersection");
841 return intersections.isEmpty() ? r.findRelationMembers("sign") : intersections;
842 default:
843 break;
844 }
845 }
846 return Collections.emptyList();
847 }
848
849 /**
850 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
851 * the result of this process in an instance of {@link SplitWayCommand}.
852 *
853 * Note that changes are not applied to the data yet. You have to
854 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
855 *
856 * Replies null if the way couldn't be split at the given nodes.
857 *
858 * @param way the way to split. Must not be null.
859 * @param atNodes the list of nodes where the way is split. Must not be null.
860 * @param selection The list of currently selected primitives
861 * @return the result from the split operation
862 */
863 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
864 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
865 return chunks != null ? splitWay(way, chunks, selection) : null;
866 }
867
868 /**
869 * Add relations that are treated in a specific way.
870 * @param relationType The value in the {@code type} key
871 * @param treatAs The type of relation to treat the {@code relationType} as.
872 * Currently only supports relations that can be handled like "restriction"
873 * relations.
874 * @return the previous value associated with relationType, or null if there was no mapping
875 * @since 15078
876 */
877 public static String addSpecialRelationType(String relationType, String treatAs) {
878 return relationSpecialTypes.put(relationType, treatAs);
879 }
880
881 /**
882 * Get the types of relations that are treated differently
883 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>}
884 * @since 15078
885 */
886 public static Map<String, String> getSpecialRelationTypes() {
887 return relationSpecialTypes;
888 }
889
890 /**
891 * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be
892 * determined without downloading missing relation members.
893 */
894 public enum WhenRelationOrderUncertain {
895 /**
896 * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to
897 * proceed without downloading anything.
898 */
899 ASK_USER_FOR_CONSENT_TO_DOWNLOAD,
900 /**
901 * If there are relation members missing, and these are needed to determine the order of the new parts in
902 * that relation, abort the split operation.
903 */
904 ABORT,
905 /**
906 * If there are relation members missing, and these are needed to determine the order of the new parts in
907 * that relation, continue with the split operation anyway, without downloading anything. Caution: use this
908 * option with care.
909 */
910 SPLIT_ANYWAY,
911 /**
912 * If there are relation members missing, and these are needed to determine the order of the new parts in
913 * that relation, automatically download these without prompting the user.
914 */
915 DOWNLOAD_MISSING_MEMBERS
916 }
917
918 static class RelationAnalysis {
919 private final Relation relation;
920 private final RelationMember relationMember;
921 private final Direction direction;
922 private final Set<Way> neededIncompleteMembers;
923
924 RelationAnalysis(Relation relation,
925 RelationMember relationMember,
926 Direction direction,
927 Set<Way> neededIncompleteMembers) {
928 this.relation = relation;
929 this.relationMember = relationMember;
930 this.direction = direction;
931 this.neededIncompleteMembers = neededIncompleteMembers;
932 }
933
934 RelationMember getRelationMember() {
935 return relationMember;
936 }
937
938 Direction getDirection() {
939 return direction;
940 }
941
942 public Set<Way> getNeededIncompleteMembers() {
943 return neededIncompleteMembers;
944 }
945
946 Relation getRelation() {
947 return relation;
948 }
949 }
950
951 enum Direction {
952 FORWARDS,
953 BACKWARDS,
954 UNKNOWN,
955 IRRELEVANT
956 }
957
958 enum WarningType {
959 GENERIC,
960 ROLE
961 }
962
963 enum MissingMemberStrategy {
964 GO_AHEAD_WITH_DOWNLOADS,
965 GO_AHEAD_WITHOUT_DOWNLOADS,
966 USER_ABORTED
967 }
968}
Note: See TracBrowser for help on using the repository browser.