source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/SplitMode.java

Last change on this file was 18761, checked in by taylor.smock, 2 years ago

Fix borked tests (see r18757)

File size: 18.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Point;
10import java.awt.event.ActionEvent;
11import java.awt.event.FocusAdapter;
12import java.awt.event.FocusEvent;
13import java.awt.event.InputEvent;
14import java.awt.event.KeyEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashSet;
22import java.util.List;
23import java.util.Optional;
24import java.util.Set;
25import java.util.stream.Collectors;
26
27import javax.swing.AbstractAction;
28import javax.swing.BorderFactory;
29import javax.swing.JMenuItem;
30import javax.swing.JOptionPane;
31import javax.swing.JPopupMenu;
32import javax.swing.border.Border;
33import javax.swing.border.TitledBorder;
34
35import org.openstreetmap.josm.actions.SplitWayAction;
36import org.openstreetmap.josm.command.AddCommand;
37import org.openstreetmap.josm.command.ChangeNodesCommand;
38import org.openstreetmap.josm.command.Command;
39import org.openstreetmap.josm.command.SequenceCommand;
40import org.openstreetmap.josm.data.UndoRedoHandler;
41import org.openstreetmap.josm.data.coor.EastNorth;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
44import org.openstreetmap.josm.data.osm.IWaySegment;
45import org.openstreetmap.josm.data.osm.Node;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Way;
48import org.openstreetmap.josm.data.preferences.BooleanProperty;
49import org.openstreetmap.josm.data.preferences.CachingProperty;
50import org.openstreetmap.josm.gui.MainApplication;
51import org.openstreetmap.josm.gui.MapView;
52import org.openstreetmap.josm.gui.MenuScroller;
53import org.openstreetmap.josm.gui.Notification;
54import org.openstreetmap.josm.gui.layer.Layer;
55import org.openstreetmap.josm.gui.util.HighlightHelper;
56import org.openstreetmap.josm.tools.Geometry;
57import org.openstreetmap.josm.tools.ImageProvider;
58import org.openstreetmap.josm.tools.PlatformManager;
59import org.openstreetmap.josm.tools.Shortcut;
60import org.openstreetmap.josm.tools.Utils;
61
62/**
63 * Map mode for splitting ways.
64 *
65 * @since 18759
66 */
67public class SplitMode extends MapMode {
68
69 /** Prioritized selected ways over others when splitting */
70 static final CachingProperty<Boolean> PREFER_SELECTED_WAYS
71 = new BooleanProperty("split-mode.prefer-selected-ways", true).cached();
72
73 /** Don't consider disabled ways */
74 static final CachingProperty<Boolean> IGNORE_DISABLED_WAYS
75 = new BooleanProperty("split-mode.ignore-disabled-ways", true).cached();
76
77 /** Helper to keep track of highlighted primitives */
78 HighlightHelper highlight = new HighlightHelper();
79
80 /**
81 * Construct a new SplitMode object
82 */
83 public SplitMode() {
84 super(tr("Split mode"), "splitway", tr("Split ways"),
85 Shortcut.registerShortcut("mapmode:split", tr("Mode: {0}", tr("Split mode")), KeyEvent.VK_T, Shortcut.DIRECT),
86 ImageProvider.getCursor("crosshair", null));
87 }
88
89 @Override
90 public void enterMode() {
91 super.enterMode();
92 MapView mv = MainApplication.getMap().mapView;
93 mv.addMouseListener(this);
94 mv.addMouseMotionListener(this);
95 }
96
97 @Override
98 public void exitMode() {
99 super.exitMode();
100 MapView mv = MainApplication.getMap().mapView;
101 mv.removeMouseMotionListener(this);
102 mv.removeMouseListener(this);
103 removeHighlighting();
104 }
105
106 @Override
107 public boolean layerIsSupported(Layer l) {
108 return isEditableDataLayer(l);
109 }
110
111 @Override
112 public void mousePressed(MouseEvent e) {
113 super.mousePressed(e);
114
115 MapView mv = MainApplication.getMap().mapView;
116 int mouseDownButton = e.getButton();
117 Point mousePos = e.getPoint();
118
119 // return early
120 if (!mv.isActiveLayerVisible() || Boolean.FALSE.equals(this.getValue("active")) || mouseDownButton != MouseEvent.BUTTON1)
121 return;
122
123 // update which modifiers are pressed (shift, alt, ctrl)
124 updateKeyModifiers(e);
125
126 DataSet ds = getLayerManager().getEditDataSet();
127 if (ds == null)
128 return;
129
130 final List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
131 Optional<OsmPrimitive> primitiveAtPoint = getPrimitiveAtPoint(e.getPoint());
132 if (!primitiveAtPoint.isPresent())
133 return;
134
135 final OsmPrimitive nearestPrimitive = primitiveAtPoint.get();
136
137 if (nearestPrimitive instanceof Node) {
138 // Split way at node
139 Node n = (Node) nearestPrimitive;
140
141 List<Way> applicableWays = getApplicableWays(n, selectedWays);
142
143 if (applicableWays.isEmpty()) {
144 new Notification(
145 tr("The selected node is not in the middle of any non-closed way."))
146 .setIcon(JOptionPane.WARNING_MESSAGE)
147 .show();
148 return;
149 }
150
151 if (applicableWays.size() > 1) {
152 createPopup(n, applicableWays).show(mv, mousePos.x, mousePos.y);
153 } else {
154 final Way splitWay = applicableWays.get(0);
155 SplitWayAction.doSplitWayShowSegmentSelection(splitWay, Collections.singletonList(n), null);
156 if (updateUserFeedback(e)) {
157 MainApplication.getMap().mapView.repaint();
158 }
159 }
160 } else if (nearestPrimitive instanceof Way && !((Way) nearestPrimitive).isClosed()) {
161 addNodeAndSplit(mv, mousePos, (Way) nearestPrimitive);
162 if (updateUserFeedback(e)) {
163 MainApplication.getMap().mapView.repaint();
164 }
165 } else if (nearestPrimitive instanceof Way) {
166 new Notification(
167 tr("Splitting closed ways is not yet implemented."))
168 .setIcon(JOptionPane.WARNING_MESSAGE)
169 .show();
170 }
171 }
172
173 /**
174 * Add a node to a way and then split it
175 * @param mv The current mapview
176 * @param mousePos The mouse position
177 * @param way The nearest way
178 */
179 private void addNodeAndSplit(MapView mv, Point mousePos, Way way) {
180 final Node mouseLatLon = new Node(mv.getLatLon(mousePos.x, mousePos.y));
181 // Insert node into way and split
182 // Get the nearest segment
183 final IWaySegment<Node, Way> closestSegment = Geometry.getClosestWaySegment(way, mouseLatLon);
184 final EastNorth en = Geometry.closestPointToLine(closestSegment.getFirstNode().getEastNorth(),
185 closestSegment.getSecondNode().getEastNorth(), mouseLatLon.getEastNorth());
186 mouseLatLon.setEastNorth(en);
187 final List<Command> commandList = new ArrayList<>();
188 // Add the node to the dataset
189 final AddCommand addPrimitivesCommand = new AddCommand(way.getDataSet(), mouseLatLon);
190 commandList.add(addPrimitivesCommand);
191 // Get common ways for the segment, but only if the nearest primitive isn't also selected
192 final Set<Way> commonParentWays = new HashSet<>(closestSegment.getFirstNode().getParentWays());
193 commonParentWays.retainAll(closestSegment.getSecondNode().getParentWays());
194 if (way.isSelected()) {
195 commonParentWays.clear();
196 commonParentWays.add(way);
197 }
198 // Add the node to each parent way
199 for (Way parentWay : commonParentWays) {
200 for (int i = 0; i < parentWay.getNodesCount() - 1; i++) {
201 IWaySegment<Node, Way> waySegment = IWaySegment.forNodePair(parentWay, parentWay.getNode(i), parentWay.getNode(i + 1));
202 if (closestSegment.isSimilar(waySegment)) {
203 final List<Node> nodes = parentWay.getNodes();
204 nodes.add(waySegment.getUpperIndex(), mouseLatLon);
205 final ChangeNodesCommand changeNodesCommand = new ChangeNodesCommand(parentWay, nodes);
206 commandList.add(changeNodesCommand);
207 }
208 }
209 }
210 UndoRedoHandler.getInstance().add(SequenceCommand.wrapIfNeeded(trn("Add node for splitting a way",
211 "Add node for splitting {0} ways", commonParentWays.size(), commonParentWays.size()), commandList));
212 if (commonParentWays.size() > 1) {
213 createPopup(mouseLatLon, commonParentWays).show(mv, mousePos.x, mousePos.y);
214 } else {
215 SplitWayAction.doSplitWayShowSegmentSelection(way, Collections.singletonList(mouseLatLon), null);
216 if (way.getDataSet().selectionEmpty()) {
217 way.getDataSet().setSelected(way);
218 }
219 }
220 }
221
222 @Override
223 public void mouseMoved(MouseEvent e) {
224 if (updateUserFeedback(e)) {
225 MainApplication.getMap().mapView.repaint();
226 }
227 }
228
229 @Override
230 public String getModeHelpText() {
231 return tr("Click on the location where a way should be split");
232 }
233
234 private static Optional<OsmPrimitive> getPrimitiveAtPoint(Point p) {
235 MapView mv = MainApplication.getMap().mapView;
236 return Optional.ofNullable(mv.getNearestNodeOrWay(p, mv.isSelectablePredicate, true));
237 }
238
239 /**
240 * Get a list of potential ways to be split for a given node
241 * @param n The node at which ways should be split
242 * @param preferredWays List of ways that should be prioritized over others.
243 * If one or more potential preferred ways are found, other ways are disregarded.
244 * @return List of potential ways to be split
245 */
246 private static List<Way> getApplicableWays(Node n, Collection<Way> preferredWays) {
247 final List<Way> parentWays = n.getParentWays();
248 List<Way> applicableWays = parentWays.stream()
249 .filter(w -> w.isDrawable() &&
250 !(w.isDisabled() && IGNORE_DISABLED_WAYS.get()) &&
251 !w.isClosed() &&
252 w.isInnerNode(n))
253 .collect(Collectors.toList());
254
255 if (Boolean.TRUE.equals(PREFER_SELECTED_WAYS.get()) && preferredWays != null) {
256 List<Way> preferredApplicableWays = applicableWays.stream()
257 .filter(preferredWays::contains).collect(Collectors.toList());
258
259 if (!preferredApplicableWays.isEmpty()) {
260 applicableWays = preferredApplicableWays;
261 }
262 }
263
264 return applicableWays;
265 }
266
267 /**
268 * Create a new split way selection popup
269 * @param n Node at which ways should be split
270 * @param applicableWays Potential split ways to select from
271 * @return A new popup object
272 */
273 private JPopupMenu createPopup(Node n, Collection<Way> applicableWays) {
274 // See also SelectAction#getModeHelpText "[there] needs to be a better way
275 final String menuKey;
276 switch (PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()) {
277 case InputEvent.CTRL_DOWN_MASK:
278 menuKey = trc("SplitMode popup", "Ctrl");
279 break;
280 case InputEvent.META_DOWN_MASK:
281 menuKey = trc("SplitMode popup", "Meta");
282 break;
283 default:
284 throw new IllegalStateException("Unknown platform menu shortcut key for " + PlatformManager.getPlatform().getOSDescription());
285 }
286
287 JPopupMenu pm = new JPopupMenu("<html>" + tr("Select way to split.<br>" +
288 "Hold {0} for multiple selection.", menuKey) + "</html>");
289
290 Border titleUnderline = BorderFactory.createMatteBorder(1, 0, 0, 0, pm.getForeground());
291 TitledBorder labelBorder = BorderFactory.createTitledBorder(titleUnderline, pm.getLabel(),
292 TitledBorder.CENTER, TitledBorder.ABOVE_TOP, pm.getFont(), pm.getForeground());
293 pm.setBorder(BorderFactory.createCompoundBorder(pm.getBorder(), labelBorder));
294
295 for (final Way w : applicableWays) {
296 JMenuItem mi = new JMenuItem(new SplitWayActionConcrete(w, Collections.singletonList(n), null));
297
298 mi.setText("<html>" + createLabelText(w) + "</html>");
299
300 addHoverHighlightListener(mi, Arrays.asList(n, w));
301
302 mi.addFocusListener(new FocusAdapter() {
303 @Override
304 public void focusGained(FocusEvent e) {
305 if (highlight.highlightOnly(Arrays.asList(n, w))) {
306 MainApplication.getMap().mapView.repaint();
307 }
308 }
309
310 @Override
311 public void focusLost(FocusEvent e) {
312 if (removeHighlighting()) {
313 MainApplication.getMap().mapView.repaint();
314 }
315 }
316 });
317
318 mi.addActionListener(actionEvent -> {
319 removeHighlighting();
320 // Prevent popup menu from closing when ctrl is pressed while selecting a way to split
321 updateKeyModifiers(actionEvent);
322 if (platformMenuShortcutKeyMask) {
323 JMenuItem source = (JMenuItem) actionEvent.getSource();
324 JPopupMenu popup = (JPopupMenu) source.getParent();
325 popup.remove(source);
326
327 // Close popup menu anyway when there are no more options left
328 if (popup.getSubElements().length > 0) {
329 popup.setVisible(true);
330 }
331 }
332 });
333
334 pm.add(mi);
335 }
336
337 MenuScroller.setScrollerFor(pm);
338 return pm;
339 }
340
341 /**
342 * Determine objects to highlight and update highlight
343 * @param e {@link MouseEvent} that triggered the update
344 * @return true if repaint is required
345 */
346 private boolean updateUserFeedback(MouseEvent e) {
347 List<OsmPrimitive> toHighlight = new ArrayList<>(2);
348
349 Optional<OsmPrimitive> pHovered = getPrimitiveAtPoint(e.getPoint());
350 DataSet ds = getLayerManager().getEditDataSet();
351
352 if (pHovered.filter(Node.class::isInstance).isPresent()) {
353 Node nHovered = (Node) pHovered.get();
354 final List<Way> selectedWays = ds != null ? new ArrayList<>(ds.getSelectedWays()) : null;
355 List<Way> applicableWays = getApplicableWays(nHovered, selectedWays);
356 if (!applicableWays.isEmpty()) {
357 pHovered.ifPresent(toHighlight::add);
358 }
359 if (applicableWays.size() == 1) {
360 toHighlight.add(applicableWays.get(0));
361 }
362 }
363
364 return highlight.highlightOnly(toHighlight);
365 }
366
367 /**
368 * Removes all existing highlights.
369 * @return true if a repaint is required
370 */
371 private boolean removeHighlighting() {
372 boolean anyHighlighted = highlight.anyHighlighted();
373 highlight.clear();
374 return anyHighlighted;
375 }
376
377 /**
378 * Add a mouse listener to the component {@code c} which highlights {@code prims}
379 * when the mouse pointer is hovering over the component
380 * @param c The component to add the hover mouse listener to
381 * @param prims The primitives to highlight when the component is hovered
382 */
383 private void addHoverHighlightListener(Component c, Collection<OsmPrimitive> prims) {
384 c.addMouseListener(new MouseAdapter() {
385 @Override
386 public void mouseEntered(MouseEvent e) {
387 if (highlight.highlightOnly(prims)) {
388 MainApplication.getMap().mapView.repaint();
389 }
390 }
391
392 @Override
393 public void mouseExited(MouseEvent e) {
394 if (removeHighlighting()) {
395 MainApplication.getMap().mapView.repaint();
396 }
397 }
398 });
399 }
400
401 /**
402 * Create the text for a {@link OsmPrimitive} label, including its keys
403 * @param primitive The {@link OsmPrimitive} to describe
404 * @return Text describing the {@link OsmPrimitive}
405 */
406 private static String createLabelText(OsmPrimitive primitive) {
407 return createLabelText(primitive, true);
408 }
409
410 /**
411 * Create the text for a {@link OsmPrimitive} label
412 * @param primitive The {@link OsmPrimitive} to describe
413 * @param includeKeys Include keys in description
414 * @return Text describing the {@link OsmPrimitive}
415 */
416 private static String createLabelText(OsmPrimitive primitive, boolean includeKeys) {
417 final StringBuilder text = new StringBuilder(32);
418 String name = Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance()));
419 if (primitive.isNewOrUndeleted() || primitive.isModified()) {
420 name = "<i><b>"+ name + "*</b></i>";
421 }
422 text.append(name);
423
424 if (!primitive.isNew()) {
425 text.append(" [id=").append(primitive.getId()).append(']');
426 }
427
428 if (primitive.getUser() != null) {
429 text.append(" [").append(tr("User:")).append(' ')
430 .append(Utils.escapeReservedCharactersHTML(primitive.getUser().getName())).append(']');
431 }
432
433 if (includeKeys) {
434 primitive.visitKeys((p, key, value) -> text.append("<br>").append(key).append('=').append(value));
435 }
436
437 return text.toString();
438 }
439
440 /**
441 * Split a specified {@link Way} at the given nodes
442 * <p>
443 * Does not attempt to figure out which ways to split based on selection like {@link SplitWayAction}
444 * and instead works on specified ways given in constructor
445 *
446 * @since 18759
447 */
448 private static class SplitWayActionConcrete extends AbstractAction {
449
450 private final Way splitWay;
451 private final List<Node> splitNodes;
452 private final List<OsmPrimitive> selection;
453
454 /**
455 * Construct an action to split way {@code splitWay} at nodes {@code splitNodes}
456 * @param splitWay The way to split
457 * @param splitNodes The nodes the way should be split at
458 * @param selection (Optional, can be null) Selection which should be updated
459 */
460 SplitWayActionConcrete(Way splitWay, List<Node> splitNodes, List<OsmPrimitive> selection) {
461 super(tr("Split way {0}", DefaultNameFormatter.getInstance().format(splitWay)),
462 ImageProvider.get(splitWay.getType()));
463 putValue(SHORT_DESCRIPTION, getValue(NAME));
464 this.splitWay = splitWay;
465 this.splitNodes = splitNodes;
466 this.selection = selection;
467 }
468
469 @Override
470 public void actionPerformed(ActionEvent e) {
471 SplitWayAction.doSplitWayShowSegmentSelection(splitWay, splitNodes, selection);
472 if (splitWay.getDataSet().selectionEmpty()) {
473 splitWay.getDataSet().setSelected(splitWay);
474 }
475 }
476
477 @Override
478 public boolean isEnabled() {
479 return !splitWay.getDataSet().isLocked();
480 }
481 }
482}
Note: See TracBrowser for help on using the repository browser.