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

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

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

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