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

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

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

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