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

Last change on this file since 15943 was 15943, checked in by GerdP, 5 years ago

see #18596 Fix relation ordering after split-way
Patch by JeroenHoek, slightly modified

  • download needed relation members if wanted
  • improve member ordering of route relations

TODO:

  • download parents if not known
  • fix also ordering of members in multipolygon relations
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, indexOfWayToKeep);
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 List<Relation> relationsNeedingMoreMembers = new ArrayList<>();
358 Set<OsmPrimitive> incompleteMembers = new HashSet<>();
359 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
360 if (!relationAnalysis.needsMoreMembers()) continue;
361
362 Relation relation = relationAnalysis.getRelation();
363 int position = relationAnalysis.getPosition();
364 int membersCount = relation.getMembersCount();
365
366 // Mark the neighbouring members for downloading if these are ways too.
367 relationsNeedingMoreMembers.add(relation);
368 RelationMember rmPrev = position == 0
369 ? relation.getMember(membersCount - 1)
370 : relation.getMember(position - 1);
371 RelationMember rmNext = position == membersCount - 1
372 ? relation.getMember(0)
373 : relation.getMember(position + 1);
374
375 if (rmPrev != null && rmPrev.isWay()) {
376 incompleteMembers.add(rmPrev.getWay());
377 }
378 if (rmNext != null && rmNext.isWay()) {
379 incompleteMembers.add(rmNext.getWay());
380 }
381 }
382
383 MissingMemberStrategy missingMemberStrategy;
384 if (relationsNeedingMoreMembers.isEmpty()) {
385 // The split can be performed without any extra downloads.
386 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
387 } else {
388 switch (whenRelationOrderUncertain) {
389 case ASK_USER_FOR_CONSENT_TO_DOWNLOAD:
390 // If the analysis shows that for some relations missing members should be downloaded, offer the user the
391 // chance to consent to this.
392
393 // Only ask the user about downloading missing members when they haven't consented to this before.
394 if (ConditionalOptionPaneUtil.getDialogReturnValue(DOWNLOAD_MISSING_PREF_KEY) == Integer.MAX_VALUE) {
395 // User has previously told us downloading missing relation members is fine.
396 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
397 } else {
398 // Ask the user.
399 missingMemberStrategy = offerToDownloadMissingMembersIfNeeded(analysis, relationsNeedingMoreMembers);
400 }
401 break;
402 case SPLIT_ANYWAY:
403 missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
404 break;
405 case DOWNLOAD_MISSING_MEMBERS:
406 missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
407 break;
408 case ABORT:
409 default:
410 missingMemberStrategy = USER_ABORTED;
411 break;
412 }
413 }
414
415 switch (missingMemberStrategy) {
416 case GO_AHEAD_WITH_DOWNLOADS:
417 try {
418 downloadMissingMembers(incompleteMembers);
419 } catch (OsmTransferException e) {
420 ExceptionDialogUtil.explainException(e);
421 return Optional.empty();
422 }
423 // If missing relation members were downloaded, perform the analysis again to find the relation
424 // member order for all relations.
425 analysis = analyseSplit(way, wayToKeep, newWays, indexOfWayToKeep);
426 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep));
427 case GO_AHEAD_WITHOUT_DOWNLOADS:
428 // Proceed with the split with the information we have.
429 // This can mean that there are no missing members we want, or that the user chooses to continue
430 // the split without downloading them.
431 return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep));
432 case USER_ABORTED:
433 default:
434 return Optional.empty();
435 }
436 }
437
438 static Analysis analyseSplit(Way way,
439 Way wayToKeep,
440 List<Way> newWays,
441 int indexOfWayToKeep) {
442 Collection<Command> commandList = new ArrayList<>();
443 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn",
444 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
445
446 // Change the original way
447 final Way changedWay = new Way(way);
448 changedWay.setNodes(wayToKeep.getNodes());
449 commandList.add(new ChangeCommand(way, changedWay));
450
451 for (Way wayToAdd : newWays) {
452 commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
453 }
454
455 List<RelationAnalysis> relationAnalyses = new ArrayList<>();
456 EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class);
457 int numberOfRelations = 0;
458
459 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) {
460 if (!r.isUsable()) {
461 continue;
462 }
463
464 numberOfRelations++;
465
466 Relation c = null;
467 String type = Optional.ofNullable(r.get("type")).orElse("");
468 // Known types of ordered relations.
469 boolean isOrderedRelation = "route".equals(type) || "multipolygon".equals(type) || "boundary".equals(type);
470
471 int ic = 0;
472 int ir = 0;
473 List<RelationMember> relationMembers = r.getMembers();
474 for (RelationMember rm : relationMembers) {
475 if (rm.isWay() && rm.getMember() == way) {
476 boolean insert = true;
477 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) {
478 RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWay);
479 if (rValue.warnme) warnings.add(WarningType.GENERIC);
480 insert = rValue.insert;
481 c = rValue.relation;
482 } else if (!isOrderedRelation) {
483 // Warn the user when relations that are not a route or multipolygon are modified as a result
484 // of splitting up the way, because we can't tell if this might break anything.
485 warnings.add(WarningType.GENERIC);
486 }
487 if (c == null) {
488 c = new Relation(r);
489 }
490
491 if (insert) {
492 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
493 warnings.add(WarningType.ROLE);
494 }
495
496 // Attempt to determine the direction the ways in the relation are ordered.
497 Direction direction = Direction.UNKNOWN;
498 if (isOrderedRelation) {
499 if (way.lastNode() == way.firstNode()) {
500 // Self-closing way.
501 direction = Direction.IRRELEVANT;
502 } else {
503 // For ordered relations, looking beyond the nearest neighbour members is not required,
504 // and can even cause the wrong direction to be guessed (with closed loops).
505 if (ir - 1 >= 0 && relationMembers.get(ir - 1).isWay()) {
506 Way w = relationMembers.get(ir - 1).getWay();
507 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
508 direction = Direction.FORWARDS;
509 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
510 direction = Direction.BACKWARDS;
511 }
512 }
513 if (ir + 1 < relationMembers.size() && relationMembers.get(ir + 1).isWay()) {
514 Way w = relationMembers.get(ir + 1).getWay();
515 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
516 direction = Direction.BACKWARDS;
517 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
518 direction = Direction.FORWARDS;
519 }
520 }
521 }
522 } else {
523 int k = 1;
524 while (ir - k >= 0 || ir + k < relationMembers.size()) {
525 if (ir - k >= 0 && relationMembers.get(ir - k).isWay()) {
526 Way w = relationMembers.get(ir - k).getWay();
527 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
528 direction = Direction.FORWARDS;
529 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
530 direction = Direction.BACKWARDS;
531 }
532 break;
533 }
534 if (ir + k < relationMembers.size() && relationMembers.get(ir + k).isWay()) {
535 Way w = relationMembers.get(ir + k).getWay();
536 if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
537 direction = Direction.BACKWARDS;
538 } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
539 direction = Direction.FORWARDS;
540 }
541 break;
542 }
543 k++;
544 }
545 }
546
547 // We don't have enough information to determine the order of the new ways in this relation.
548 // This may cause relations to be saved with the two new way sections in reverse order.
549 //
550 // This often breaks routes.
551 //
552 // The user should be asked to download more members, or to abort the split operation.
553 boolean needsMoreMembers = isOrderedRelation
554 && direction == Direction.UNKNOWN
555 && relationMembers.size() > 1
556 && r.hasIncompleteMembers();
557
558 relationAnalyses.add(new RelationAnalysis(c, rm, direction, needsMoreMembers, ic));
559 ic += indexOfWayToKeep;
560 }
561 }
562 ic++;
563 ir++;
564 }
565
566 if (c != null) {
567 commandList.add(new ChangeCommand(r.getDataSet(), r, c));
568 }
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 List<RelationAnalysis> getRelationAnalyses() {
591 return relationAnalyses;
592 }
593
594 Collection<Command> getCommands() {
595 return commands;
596 }
597
598 EnumSet<WarningType> getWarningTypes() {
599 return warningTypes;
600 }
601
602 public int getNumberOfRelations() {
603 return numberOfRelations;
604 }
605 }
606
607 static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis,
608 List<Relation> relationsNeedingMoreMembers) {
609 String[] options = {
610 tr("Yes, download the missing members"),
611 tr("No, abort the split operation"),
612 tr("No, perform the split without downloading")
613 };
614
615 String msgMemberOfRelations = trn(
616 "This way is part of a relation.",
617 "This way is part of {0} relations.",
618 analysis.getNumberOfRelations(),
619 analysis.getNumberOfRelations()
620 );
621
622 String msgReferToRelations;
623 if (analysis.getNumberOfRelations() == 1) {
624 msgReferToRelations = tr("this relation");
625 } else if (analysis.getNumberOfRelations() == relationsNeedingMoreMembers.size()) {
626 msgReferToRelations = tr("these relations");
627 } else {
628 msgReferToRelations = trn(
629 "one relation",
630 "{0} relations",
631 relationsNeedingMoreMembers.size(),
632 relationsNeedingMoreMembers.size()
633 );
634 }
635
636 String msgRelationsMissingData = tr(
637 "For {0} the correct order of the new way parts could not be determined. " +
638 "To fix this, some missing relation members should be downloaded first.",
639 msgReferToRelations
640 );
641
642 JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData);
643 msg.setMaxWidth(600);
644
645 int ret = JOptionPane.showOptionDialog(
646 MainApplication.getMainFrame(),
647 msg,
648 tr("Download missing relation members?"),
649 JOptionPane.OK_CANCEL_OPTION,
650 JOptionPane.QUESTION_MESSAGE,
651 null,
652 options,
653 options[0]
654 );
655
656 switch (ret) {
657 case JOptionPane.OK_OPTION:
658 // Ask the user if they want to do this automatically from now on. We only ask this for the download
659 // action, because automatically cancelling is confusing (the user can't tell why this happened), and
660 // automatically performing the split without downloading missing members despite needing them is
661 // likely to break a lot of routes. The user also can't tell the difference between a split that needs
662 // no downloads at all, and this special case where downloading missing relation members will prevent
663 // broken relations.
664 ConditionalOptionPaneUtil.showMessageDialog(
665 DOWNLOAD_MISSING_PREF_KEY,
666 MainApplication.getMainFrame(),
667 tr("Missing relation members will be downloaded. Should this be done automatically from now on?"),
668 tr("Downloading missing relation members"),
669 JOptionPane.INFORMATION_MESSAGE
670 );
671 return GO_AHEAD_WITH_DOWNLOADS;
672 case JOptionPane.CANCEL_OPTION:
673 return GO_AHEAD_WITHOUT_DOWNLOADS;
674 default:
675 return USER_ABORTED;
676 }
677 }
678
679 static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException {
680 // Download the missing members.
681 MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create();
682 reader.append(incompleteMembers);
683
684 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
685 MainApplication.getLayerManager().getEditLayer().mergeFrom(ds);
686 }
687
688 static SplitWayCommand splitBasedOnAnalyses(Way way,
689 List<Way> newWays,
690 List<OsmPrimitive> newSelection,
691 Analysis analysis,
692 int indexOfWayToKeep) {
693 if (newSelection != null && !newSelection.contains(way)) {
694 newSelection.add(way);
695 }
696
697 if (newSelection != null) {
698 newSelection.addAll(newWays);
699 }
700
701 // Perform the split.
702 for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
703 RelationMember rm = relationAnalysis.getRelationMember();
704 Relation relation = relationAnalysis.getRelation();
705 Direction direction = relationAnalysis.getDirection();
706 int position = relationAnalysis.getPosition();
707
708 int j = position;
709 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
710 for (Way wayToAdd : waysToAddBefore) {
711 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
712 j++;
713 if (direction == Direction.BACKWARDS) {
714 relation.addMember(position + 1, em);
715 } else {
716 relation.addMember(j - 1, em);
717 }
718 }
719 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
720 for (Way wayToAdd : waysToAddAfter) {
721 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
722 j++;
723 if (direction == Direction.BACKWARDS) {
724 relation.addMember(position, em);
725 } else {
726 relation.addMember(j, em);
727 }
728 }
729 }
730
731 EnumSet<WarningType> warnings = analysis.getWarningTypes();
732
733 if (warnings.contains(WarningType.ROLE)) {
734 warningNotifier.accept(
735 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
736 } else if (warnings.contains(WarningType.GENERIC)) {
737 warningNotifier.accept(
738 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
739 }
740
741 return new SplitWayCommand(
742 /* for correct i18n of plural forms - see #9110 */
743 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
744 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
745 analysis.getCommands(),
746 newSelection,
747 way,
748 newWays
749 );
750 }
751
752 private static RelationInformation treatAsRestriction(Relation r,
753 RelationMember rm, Relation c, Collection<Way> newWays, Way way,
754 Way changedWay) {
755 RelationInformation relationInformation = new RelationInformation();
756 /* this code assumes the restriction is correct. No real error checking done */
757 String role = rm.getRole();
758 String type = Optional.ofNullable(r.get("type")).orElse("");
759 if ("from".equals(role) || "to".equals(role)) {
760 List<Node> nodes = new ArrayList<>();
761 for (OsmPrimitive via : findVias(r, type)) {
762 if (via instanceof Node) {
763 nodes.add((Node) via);
764 } else if (via instanceof Way) {
765 nodes.add(((Way) via).lastNode());
766 nodes.add(((Way) via).firstNode());
767 }
768 }
769 Way res = null;
770 for (Node n : nodes) {
771 if (changedWay.isFirstLastNode(n)) {
772 res = way;
773 }
774 }
775 if (res == null) {
776 for (Way wayToAdd : newWays) {
777 for (Node n : nodes) {
778 if (wayToAdd.isFirstLastNode(n)) {
779 res = wayToAdd;
780 }
781 }
782 }
783 if (res != null) {
784 if (c == null) {
785 c = new Relation(r);
786 }
787 c.addMember(new RelationMember(role, res));
788 c.removeMembersFor(way);
789 relationInformation.insert = false;
790 }
791 } else {
792 relationInformation.insert = false;
793 }
794 } else if (!"via".equals(role)) {
795 relationInformation.warnme = true;
796 }
797 relationInformation.relation = c;
798 return relationInformation;
799 }
800
801 static List<? extends OsmPrimitive> findVias(Relation r, String type) {
802 if (type != null) {
803 switch (type) {
804 case "connectivity":
805 case "restriction":
806 return r.findRelationMembers("via");
807 case "destination_sign":
808 // Prefer intersection over sign, see #12347
809 List<? extends OsmPrimitive> intersections = r.findRelationMembers("intersection");
810 return intersections.isEmpty() ? r.findRelationMembers("sign") : intersections;
811 default:
812 break;
813 }
814 }
815 return Collections.emptyList();
816 }
817
818 /**
819 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
820 * the result of this process in an instance of {@link SplitWayCommand}.
821 *
822 * Note that changes are not applied to the data yet. You have to
823 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
824 *
825 * Replies null if the way couldn't be split at the given nodes.
826 *
827 * @param way the way to split. Must not be null.
828 * @param atNodes the list of nodes where the way is split. Must not be null.
829 * @param selection The list of currently selected primitives
830 * @return the result from the split operation
831 */
832 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
833 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
834 return chunks != null ? splitWay(way, chunks, selection) : null;
835 }
836
837 /**
838 * Add relations that are treated in a specific way.
839 * @param relationType The value in the {@code type} key
840 * @param treatAs The type of relation to treat the {@code relationType} as.
841 * Currently only supports relations that can be handled like "restriction"
842 * relations.
843 * @return the previous value associated with relationType, or null if there was no mapping
844 * @since 15078
845 */
846 public static String addSpecialRelationType(String relationType, String treatAs) {
847 return relationSpecialTypes.put(relationType, treatAs);
848 }
849
850 /**
851 * Get the types of relations that are treated differently
852 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>}
853 * @since 15078
854 */
855 public static Map<String, String> getSpecialRelationTypes() {
856 return relationSpecialTypes;
857 }
858
859 /**
860 * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be
861 * determined without downloading missing relation members.
862 */
863 public enum WhenRelationOrderUncertain {
864 /**
865 * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to
866 * proceed without downloading anything.
867 */
868 ASK_USER_FOR_CONSENT_TO_DOWNLOAD,
869 /**
870 * If there are relation members missing, and these are needed to determine the order of the new parts in
871 * that relation, abort the split operation.
872 */
873 ABORT,
874 /**
875 * If there are relation members missing, and these are needed to determine the order of the new parts in
876 * that relation, continue with the split operation anyway, without downloading anything. Caution: use this
877 * option with care.
878 */
879 SPLIT_ANYWAY,
880 /**
881 * If there are relation members missing, and these are needed to determine the order of the new parts in
882 * that relation, automatically download these without prompting the user.
883 */
884 DOWNLOAD_MISSING_MEMBERS
885 }
886
887 static class RelationAnalysis {
888 private final Relation relation;
889 private final RelationMember relationMember;
890 private final Direction direction;
891 private final boolean needsMoreMembers;
892 private final int position;
893
894 RelationAnalysis(Relation relation,
895 RelationMember relationMember,
896 Direction direction,
897 boolean needsMoreMembers,
898 int position) {
899 this.relation = relation;
900 this.relationMember = relationMember;
901 this.direction = direction;
902 this.needsMoreMembers = needsMoreMembers;
903 this.position = position;
904 }
905
906 RelationMember getRelationMember() {
907 return relationMember;
908 }
909
910 Direction getDirection() {
911 return direction;
912 }
913
914 boolean needsMoreMembers() {
915 return needsMoreMembers;
916 }
917
918 Relation getRelation() {
919 return relation;
920 }
921
922 int getPosition() {
923 return position;
924 }
925 }
926
927 enum Direction {
928 FORWARDS,
929 BACKWARDS,
930 UNKNOWN,
931 IRRELEVANT
932 }
933
934 enum WarningType {
935 GENERIC,
936 ROLE
937 }
938
939 enum MissingMemberStrategy {
940 GO_AHEAD_WITH_DOWNLOADS,
941 GO_AHEAD_WITHOUT_DOWNLOADS,
942 USER_ABORTED
943 }
944}
Note: See TracBrowser for help on using the repository browser.