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

Last change on this file since 14654 was 14654, checked in by simon04, 5 years ago

Deprecate OsmPrimitive.getFilteredList/getFilteredSet in favour of Stream

Most use-cases involved filtering referring primitives. This can now be
accomplished using OsmPrimitive.referrers involving the Stream API and
thus avoids creation of intermediate collections.

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