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

Last change on this file since 15078 was 15078, checked in by Don-vip, 5 years ago

fix #17718 - Make it easier to add specially treated relations to SplitWayCommand (patch by taylor.smock)

File size: 21.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.Iterator;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Map;
17import java.util.Objects;
18import java.util.Optional;
19import java.util.Set;
20import java.util.function.Consumer;
21
22import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.PrimitiveId;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationMember;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.spi.preferences.Config;
30import org.openstreetmap.josm.tools.CheckParameterUtil;
31import org.openstreetmap.josm.tools.Logging;
32
33/**
34 * Splits a way into multiple ways (all identical except for their node list).
35 *
36 * Ways are just split at the selected nodes. The nodes remain in their
37 * original order. Selected nodes at the end of a way are ignored.
38 *
39 * @since 12828 ({@code SplitWayAction} converted to a {@link Command})
40 */
41public class SplitWayCommand extends SequenceCommand {
42
43 private static volatile Consumer<String> warningNotifier = Logging::warn;
44
45 /**
46 * Sets the global warning notifier.
47 * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null
48 */
49 public static void setWarningNotifier(Consumer<String> notifier) {
50 warningNotifier = Objects.requireNonNull(notifier);
51 }
52
53 private final List<? extends PrimitiveId> newSelection;
54 private final Way originalWay;
55 private final List<Way> newWays;
56 /** Map&lt;Restriction type, type to treat it as&gt; */
57 private static final Map<String, String> relationSpecialTypes = new HashMap<>();
58 static {
59 relationSpecialTypes.put("restriction", "restriction");
60 relationSpecialTypes.put("destination_sign", "restriction");
61 }
62
63 /**
64 * Create a new {@code SplitWayCommand}.
65 * @param name The description text
66 * @param commandList The sequence of commands that should be executed.
67 * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
68 * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
69 * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay})
70 */
71 public SplitWayCommand(String name, Collection<Command> commandList,
72 List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
73 super(name, commandList);
74 this.newSelection = newSelection;
75 this.originalWay = originalWay;
76 this.newWays = newWays;
77 }
78
79 /**
80 * Replies the new list of selected primitives ids
81 * @return The new list of selected primitives ids
82 */
83 public List<? extends PrimitiveId> getNewSelection() {
84 return newSelection;
85 }
86
87 /**
88 * Replies the original way being split
89 * @return The original way being split
90 */
91 public Way getOriginalWay() {
92 return originalWay;
93 }
94
95 /**
96 * Replies the resulting new ways
97 * @return The resulting new ways
98 */
99 public List<Way> getNewWays() {
100 return newWays;
101 }
102
103 /**
104 * Determines which way chunk should reuse the old id and its history
105 */
106 @FunctionalInterface
107 public interface Strategy {
108
109 /**
110 * Determines which way chunk should reuse the old id and its history.
111 *
112 * @param wayChunks the way chunks
113 * @return the way to keep
114 */
115 Way determineWayToKeep(Iterable<Way> wayChunks);
116
117 /**
118 * Returns a strategy which selects the way chunk with the highest node count to keep.
119 * @return strategy which selects the way chunk with the highest node count to keep
120 */
121 static Strategy keepLongestChunk() {
122 return wayChunks -> {
123 Way wayToKeep = null;
124 for (Way i : wayChunks) {
125 if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
126 wayToKeep = i;
127 }
128 }
129 return wayToKeep;
130 };
131 }
132
133 /**
134 * Returns a strategy which selects the first way chunk.
135 * @return strategy which selects the first way chunk
136 */
137 static Strategy keepFirstChunk() {
138 return wayChunks -> wayChunks.iterator().next();
139 }
140 }
141
142 /**
143 * Splits the nodes of {@code wayToSplit} into a list of node sequences
144 * which are separated at the nodes in {@code splitPoints}.
145 *
146 * This method displays warning messages if {@code wayToSplit} and/or
147 * {@code splitPoints} aren't consistent.
148 *
149 * Returns null, if building the split chunks fails.
150 *
151 * @param wayToSplit the way to split. Must not be null.
152 * @param splitPoints the nodes where the way is split. Must not be null.
153 * @return the list of chunks
154 */
155 public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
156 CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
157 CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
158
159 Set<Node> nodeSet = new HashSet<>(splitPoints);
160 List<List<Node>> wayChunks = new LinkedList<>();
161 List<Node> currentWayChunk = new ArrayList<>();
162 wayChunks.add(currentWayChunk);
163
164 Iterator<Node> it = wayToSplit.getNodes().iterator();
165 while (it.hasNext()) {
166 Node currentNode = it.next();
167 boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
168 currentWayChunk.add(currentNode);
169 if (nodeSet.contains(currentNode) && !atEndOfWay) {
170 currentWayChunk = new ArrayList<>();
171 currentWayChunk.add(currentNode);
172 wayChunks.add(currentWayChunk);
173 }
174 }
175
176 // Handle circular ways specially.
177 // If you split at a circular way at two nodes, you just want to split
178 // it at these points, not also at the former endpoint.
179 // So if the last node is the same first node, join the last and the
180 // first way chunk.
181 List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
182 if (wayChunks.size() >= 2
183 && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
184 && !nodeSet.contains(wayChunks.get(0).get(0))) {
185 if (wayChunks.size() == 2) {
186 warningNotifier.accept(tr("You must select two or more nodes to split a circular way."));
187 return null;
188 }
189 lastWayChunk.remove(lastWayChunk.size() - 1);
190 lastWayChunk.addAll(wayChunks.get(0));
191 wayChunks.remove(wayChunks.size() - 1);
192 wayChunks.set(0, lastWayChunk);
193 }
194
195 if (wayChunks.size() < 2) {
196 if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
197 warningNotifier.accept(
198 tr("You must select two or more nodes to split a circular way."));
199 } else {
200 warningNotifier.accept(
201 tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"));
202 }
203 return null;
204 }
205 return wayChunks;
206 }
207
208 /**
209 * Creates new way objects for the way chunks and transfers the keys from the original way.
210 * @param way the original way whose keys are transferred
211 * @param wayChunks the way chunks
212 * @return the new way objects
213 */
214 public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
215 final List<Way> newWays = new ArrayList<>();
216 for (List<Node> wayChunk : wayChunks) {
217 Way wayToAdd = new Way();
218 wayToAdd.setKeys(way.getKeys());
219 wayToAdd.setNodes(wayChunk);
220 newWays.add(wayToAdd);
221 }
222 return newWays;
223 }
224
225 /**
226 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
227 * the result of this process in an instance of {@link SplitWayCommand}.
228 *
229 * Note that changes are not applied to the data yet. You have to
230 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
231 *
232 * @param way the way to split. Must not be null.
233 * @param wayChunks the list of way chunks into the way is split. Must not be null.
234 * @param selection The list of currently selected primitives
235 * @return the result from the split operation
236 */
237 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
238 return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
239 }
240
241 /**
242 * Splits the way {@code way} into chunks of {@code wayChunks} and replies
243 * the result of this process in an instance of {@link SplitWayCommand}.
244 * The {@link SplitWayCommand.Strategy} is used to determine which
245 * way chunk should reuse the old id and its history.
246 *
247 * Note that changes are not applied to the data yet. You have to
248 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
249 *
250 * @param way the way to split. Must not be null.
251 * @param wayChunks the list of way chunks into the way is split. Must not be null.
252 * @param selection The list of currently selected primitives
253 * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
254 * @return the result from the split operation
255 */
256 public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks,
257 Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
258 // build a list of commands, and also a new selection list
259 final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
260 newSelection.addAll(selection);
261
262 // Create all potential new ways
263 final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
264
265 // Determine which part reuses the existing way
266 final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
267
268 return wayToKeep != null ? doSplitWay(way, wayToKeep, newWays, newSelection) : null;
269 }
270
271 /**
272 * Effectively constructs the {@link SplitWayCommand}.
273 * This method is only public for {@code SplitWayAction}.
274 *
275 * @param way the way to split. Must not be null.
276 * @param wayToKeep way chunk which should reuse the old id and its history
277 * @param newWays potential new ways
278 * @param newSelection new selection list to update (optional: can be null)
279 * @return the {@code SplitWayCommand}
280 */
281 public static SplitWayCommand doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
282
283 Collection<Command> commandList = new ArrayList<>(newWays.size());
284 Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn",
285 Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
286
287 // Change the original way
288 final Way changedWay = new Way(way);
289 changedWay.setNodes(wayToKeep.getNodes());
290 commandList.add(new ChangeCommand(way, changedWay));
291 if (/*!isMapModeDraw &&*/ newSelection != null && !newSelection.contains(way)) {
292 newSelection.add(way);
293 }
294 final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
295 newWays.remove(wayToKeep);
296
297 if (/*!isMapModeDraw &&*/ newSelection != null) {
298 newSelection.addAll(newWays);
299 }
300 for (Way wayToAdd : newWays) {
301 commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
302 }
303
304 boolean warnmerole = false;
305 boolean warnme = false;
306 // now copy all relations to new way also
307
308 for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) {
309 if (!r.isUsable()) {
310 continue;
311 }
312 Relation c = null;
313 String type = Optional.ofNullable(r.get("type")).orElse("");
314
315 int ic = 0;
316 int ir = 0;
317 List<RelationMember> relationMembers = r.getMembers();
318 for (RelationMember rm: relationMembers) {
319 if (rm.isWay() && rm.getMember() == way) {
320 boolean insert = true;
321 if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) {
322 Map<String, Boolean> rValue = treatAsRestriction(r, rm, c, newWays, way, changedWay);
323 warnme = rValue.containsKey("warnme") ? rValue.get("warnme") : warnme;
324 insert = rValue.containsKey("insert") ? rValue.get("insert") : insert;
325 } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
326 warnme = true;
327 }
328 if (c == null) {
329 c = new Relation(r);
330 }
331
332 if (insert) {
333 if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
334 warnmerole = true;
335 }
336
337 Boolean backwards = null;
338 int k = 1;
339 while (ir - k >= 0 || ir + k < relationMembers.size()) {
340 if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) {
341 Way w = relationMembers.get(ir - k).getWay();
342 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
343 backwards = Boolean.FALSE;
344 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
345 backwards = Boolean.TRUE;
346 }
347 break;
348 }
349 if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) {
350 Way w = relationMembers.get(ir + k).getWay();
351 if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
352 backwards = Boolean.TRUE;
353 } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
354 backwards = Boolean.FALSE;
355 }
356 break;
357 }
358 k++;
359 }
360
361 int j = ic;
362 final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
363 for (Way wayToAdd : waysToAddBefore) {
364 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
365 j++;
366 if (Boolean.TRUE.equals(backwards)) {
367 c.addMember(ic + 1, em);
368 } else {
369 c.addMember(j - 1, em);
370 }
371 }
372 final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
373 for (Way wayToAdd : waysToAddAfter) {
374 RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
375 j++;
376 if (Boolean.TRUE.equals(backwards)) {
377 c.addMember(ic, em);
378 } else {
379 c.addMember(j, em);
380 }
381 }
382 ic = j;
383 }
384 }
385 ic++;
386 ir++;
387 }
388
389 if (c != null) {
390 commandList.add(new ChangeCommand(r.getDataSet(), r, c));
391 }
392 }
393 if (warnmerole) {
394 warningNotifier.accept(
395 tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
396 } else if (warnme) {
397 warningNotifier.accept(
398 tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
399 }
400
401 return new SplitWayCommand(
402 /* for correct i18n of plural forms - see #9110 */
403 trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
404 way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
405 commandList,
406 newSelection,
407 way,
408 newWays
409 );
410 }
411
412 private static Map<String, Boolean> treatAsRestriction(Relation r,
413 RelationMember rm, Relation c, Collection<Way> newWays, Way way,
414 Way changedWay) {
415 HashMap<String, Boolean> rMap = new HashMap<>();
416 /* this code assumes the restriction is correct. No real error checking done */
417 String role = rm.getRole();
418 String type = Optional.ofNullable(r.get("type")).orElse("");
419 if ("from".equals(role) || "to".equals(role)) {
420 OsmPrimitive via = findVia(r, type);
421 List<Node> nodes = new ArrayList<>();
422 if (via != null) {
423 if (via instanceof Node) {
424 nodes.add((Node) via);
425 } else if (via instanceof Way) {
426 nodes.add(((Way) via).lastNode());
427 nodes.add(((Way) via).firstNode());
428 }
429 }
430 Way res = null;
431 for (Node n : nodes) {
432 if (changedWay.isFirstLastNode(n)) {
433 res = way;
434 }
435 }
436 if (res == null) {
437 for (Way wayToAdd : newWays) {
438 for (Node n : nodes) {
439 if (wayToAdd.isFirstLastNode(n)) {
440 res = wayToAdd;
441 }
442 }
443 }
444 if (res != null) {
445 if (c == null) {
446 c = new Relation(r);
447 }
448 c.addMember(new RelationMember(role, res));
449 c.removeMembersFor(way);
450 rMap.put("insert", false);
451 }
452 } else {
453 rMap.put("insert", false);
454 }
455 } else if (!"via".equals(role)) {
456 rMap.put("warnme", true);
457 }
458 return rMap;
459 }
460
461 static OsmPrimitive findVia(Relation r, String type) {
462 if (type != null) {
463 switch (type) {
464 case "restriction":
465 return findRelationMember(r, "via").orElse(null);
466 case "destination_sign":
467 // Prefer intersection over sign, see #12347
468 return findRelationMember(r, "intersection").orElse(findRelationMember(r, "sign").orElse(null));
469 default:
470 return null;
471 }
472 }
473 return null;
474 }
475
476 static Optional<OsmPrimitive> findRelationMember(Relation r, String role) {
477 return r.getMembers().stream().filter(rmv -> role.equals(rmv.getRole()))
478 .map(RelationMember::getMember).findAny();
479 }
480
481 /**
482 * Splits the way {@code way} at the nodes in {@code atNodes} and replies
483 * the result of this process in an instance of {@link SplitWayCommand}.
484 *
485 * Note that changes are not applied to the data yet. You have to
486 * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
487 *
488 * Replies null if the way couldn't be split at the given nodes.
489 *
490 * @param way the way to split. Must not be null.
491 * @param atNodes the list of nodes where the way is split. Must not be null.
492 * @param selection The list of currently selected primitives
493 * @return the result from the split operation
494 */
495 public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
496 List<List<Node>> chunks = buildSplitChunks(way, atNodes);
497 return chunks != null ? splitWay(way, chunks, selection) : null;
498 }
499
500 /**
501 * Add relations that are treated in a specific way.
502 * @param relationType The value in the {@code type} key
503 * @param treatAs The type of relation to treat the {@code relationType} as.
504 * Currently only supports relations that can be handled like "restriction"
505 * relations.
506 * @return the previous value associated with relationType, or null if there was no mapping
507 * @since 15078
508 */
509 public static String addSpecialRelationType(String relationType, String treatAs) {
510 return relationSpecialTypes.put(relationType, treatAs);
511 }
512
513 /**
514 * Get the types of relations that are treated differently
515 * @return {@code Map<Relation Type, Type of Relation it is to be treated as>}
516 * @since 15078
517 */
518 public static Map<String, String> getSpecialRelationTypes() {
519 return relationSpecialTypes;
520 }
521}
Note: See TracBrowser for help on using the repository browser.