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

Last change on this file since 13691 was 13173, checked in by Don-vip, 6 years ago

see #15310 - remove most of deprecated APIs

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