001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.mapmode;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Cursor;
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.awt.event.MouseEvent;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Set;
015import java.util.stream.Collectors;
016
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.command.DeleteCommand;
019import org.openstreetmap.josm.data.UndoRedoHandler;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.WaySegment;
025import org.openstreetmap.josm.gui.MainApplication;
026import org.openstreetmap.josm.gui.MapFrame;
027import org.openstreetmap.josm.gui.MapView;
028import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
029import org.openstreetmap.josm.gui.layer.Layer;
030import org.openstreetmap.josm.gui.layer.MainLayerManager;
031import org.openstreetmap.josm.gui.layer.OsmDataLayer;
032import org.openstreetmap.josm.gui.util.HighlightHelper;
033import org.openstreetmap.josm.gui.util.ModifierExListener;
034import org.openstreetmap.josm.spi.preferences.Config;
035import org.openstreetmap.josm.tools.CheckParameterUtil;
036import org.openstreetmap.josm.tools.ImageProvider;
037import org.openstreetmap.josm.tools.Shortcut;
038
039/**
040 * A map mode that enables the user to delete nodes and other objects.
041 *
042 * The user can click on an object, which gets deleted if possible. When Ctrl is
043 * pressed when releasing the button, the objects and all its references are deleted.
044 *
045 * If the user did not press Ctrl and the object has any references, the user
046 * is informed and nothing is deleted.
047 *
048 * If the user enters the mapmode and any object is selected, all selected
049 * objects are deleted, if possible.
050 *
051 * @author imi
052 */
053public class DeleteAction extends MapMode implements ModifierExListener {
054    // Cache previous mouse event (needed when only the modifier keys are pressed but the mouse isn't moved)
055    private MouseEvent oldEvent;
056
057    /**
058     * elements that have been highlighted in the previous iteration. Used
059     * to remove the highlight from them again as otherwise the whole data
060     * set would have to be checked.
061     */
062    private transient WaySegment oldHighlightedWaySegment;
063
064    private static final HighlightHelper HIGHLIGHT_HELPER = new HighlightHelper();
065    private boolean drawTargetHighlight;
066
067    enum DeleteMode {
068        none(/* ICON(cursor/modifier/) */ "delete"),
069        segment(/* ICON(cursor/modifier/) */ "delete_segment"),
070        node(/* ICON(cursor/modifier/) */ "delete_node"),
071        node_with_references(/* ICON(cursor/modifier/) */ "delete_node"),
072        way(/* ICON(cursor/modifier/) */ "delete_way_only"),
073        way_with_references(/* ICON(cursor/modifier/) */ "delete_way_normal"),
074        way_with_nodes(/* ICON(cursor/modifier/) */ "delete_way_node_only");
075
076        private final Cursor c;
077
078        DeleteMode(String cursorName) {
079            c = ImageProvider.getCursor("normal", cursorName);
080        }
081
082        /**
083         * Returns the mode cursor.
084         * @return the mode cursor
085         */
086        public Cursor cursor() {
087            return c;
088        }
089    }
090
091    private static class DeleteParameters {
092        private DeleteMode mode;
093        private Node nearestNode;
094        private WaySegment nearestSegment;
095    }
096
097    /**
098     * Construct a new DeleteAction. Mnemonic is the delete - key.
099     * @since 11713
100     */
101    public DeleteAction() {
102        super(tr("Delete Mode"),
103                "delete",
104                tr("Delete nodes or ways."),
105                Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}", tr("Delete")),
106                KeyEvent.VK_DELETE, Shortcut.CTRL),
107                ImageProvider.getCursor("normal", "delete"));
108    }
109
110    @Override
111    public void enterMode() {
112        super.enterMode();
113        if (!isEnabled())
114            return;
115
116        drawTargetHighlight = Config.getPref().getBoolean("draw.target-highlight", true);
117
118        MapFrame map = MainApplication.getMap();
119        map.mapView.addMouseListener(this);
120        map.mapView.addMouseMotionListener(this);
121        // This is required to update the cursors when ctrl/shift/alt is pressed
122        map.keyDetector.addModifierExListener(this);
123    }
124
125    @Override
126    public void exitMode() {
127        super.exitMode();
128        MapFrame map = MainApplication.getMap();
129        map.mapView.removeMouseListener(this);
130        map.mapView.removeMouseMotionListener(this);
131        map.keyDetector.removeModifierExListener(this);
132        removeHighlighting();
133    }
134
135    @Override
136    public void actionPerformed(ActionEvent e) {
137        super.actionPerformed(e);
138        doActionPerformed(e);
139    }
140
141    /**
142     * Invoked when the action occurs.
143     * @param e Action event
144     */
145    public void doActionPerformed(ActionEvent e) {
146        MainLayerManager lm = MainApplication.getLayerManager();
147        OsmDataLayer editLayer = lm.getEditLayer();
148        if (editLayer == null) {
149            return;
150        }
151
152        updateKeyModifiers(e);
153
154        Command c;
155        if (ctrl) {
156            c = DeleteCommand.deleteWithReferences(lm.getEditDataSet().getSelected());
157        } else {
158            c = DeleteCommand.delete(lm.getEditDataSet().getSelected(), !alt /* also delete nodes in way */);
159        }
160        // if c is null, an error occurred or the user aborted. Don't do anything in that case.
161        if (c != null) {
162            UndoRedoHandler.getInstance().add(c);
163            //FIXME: This should not be required, DeleteCommand should update the selection, otherwise undo/redo won't work.
164            lm.getEditDataSet().setSelected();
165        }
166    }
167
168    @Override
169    public void mouseDragged(MouseEvent e) {
170        mouseMoved(e);
171    }
172
173    /**
174     * Listen to mouse move to be able to update the cursor (and highlights)
175     * @param e The mouse event that has been captured
176     */
177    @Override
178    public void mouseMoved(MouseEvent e) {
179        oldEvent = e;
180        giveUserFeedback(e);
181    }
182
183    /**
184     * removes any highlighting that may have been set beforehand.
185     */
186    private void removeHighlighting() {
187        HIGHLIGHT_HELPER.clear();
188        DataSet ds = getLayerManager().getEditDataSet();
189        if (ds != null) {
190            ds.clearHighlightedWaySegments();
191        }
192    }
193
194    /**
195     * handles everything related to highlighting primitives and way
196     * segments for the given pointer position (via MouseEvent) and modifiers.
197     * @param e current mouse event
198     * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
199     */
200    private void addHighlighting(MouseEvent e, int modifiers) {
201        if (!drawTargetHighlight)
202            return;
203
204        DeleteParameters parameters = getDeleteParameters(e, modifiers);
205
206        if (parameters.mode == DeleteMode.segment) {
207            // deleting segments is the only action not working on OsmPrimitives
208            // so we have to handle them separately.
209            repaintIfRequired(Collections.emptySet(), parameters.nearestSegment);
210        } else {
211            // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support
212            // silent operation and SplitWayAction will show dialogs. A lot.
213            Command delCmd = buildDeleteCommands(e, modifiers, true);
214            // all other cases delete OsmPrimitives directly, so we can safely do the following
215            repaintIfRequired(delCmd == null ? Collections.emptySet() : new HashSet<>(delCmd.getParticipatingPrimitives()), null);
216        }
217    }
218
219    private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
220        boolean needsRepaint = false;
221        OsmDataLayer editLayer = getLayerManager().getEditLayer();
222
223        if (newHighlightedWaySegment == null && oldHighlightedWaySegment != null) {
224            if (editLayer != null) {
225                editLayer.data.clearHighlightedWaySegments();
226                needsRepaint = true;
227            }
228            oldHighlightedWaySegment = null;
229        } else if (newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) {
230            if (editLayer != null) {
231                editLayer.data.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment));
232                needsRepaint = true;
233            }
234            oldHighlightedWaySegment = newHighlightedWaySegment;
235        }
236        needsRepaint |= HIGHLIGHT_HELPER.highlightOnly(newHighlights);
237        if (needsRepaint && editLayer != null) {
238            editLayer.invalidate();
239        }
240    }
241
242    /**
243     * This function handles all work related to updating the cursor and highlights
244     *
245     * @param e current mouse event
246     * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
247     */
248    private void updateCursor(MouseEvent e, int modifiers) {
249        if (!MainApplication.isDisplayingMapView())
250            return;
251        MapFrame map = MainApplication.getMap();
252        if (!map.mapView.isActiveLayerVisible() || e == null)
253            return;
254
255        DeleteParameters parameters = getDeleteParameters(e, modifiers);
256        map.mapView.setNewCursor(parameters.mode.cursor(), this);
257    }
258
259    /**
260     * Gives the user feedback for the action he/she is about to do. Currently
261     * calls the cursor and target highlighting routines. Allows for modifiers
262     * not taken from the given mouse event.
263     *
264     * Normally the mouse event also contains the modifiers. However, when the
265     * mouse is not moved and only modifier keys are pressed, no mouse event
266     * occurs. We can use AWTEvent to catch those but still lack a proper
267     * mouseevent. Instead we copy the previous event and only update the modifiers.
268     * @param e mouse event
269     * @param modifiers mouse modifiers
270     */
271    private void giveUserFeedback(MouseEvent e, int modifiers) {
272        updateCursor(e, modifiers);
273        addHighlighting(e, modifiers);
274    }
275
276    /**
277     * Gives the user feedback for the action he/she is about to do. Currently
278     * calls the cursor and target highlighting routines. Extracts modifiers
279     * from mouse event.
280     * @param e mouse event
281     */
282    private void giveUserFeedback(MouseEvent e) {
283        giveUserFeedback(e, e.getModifiersEx());
284    }
285
286    /**
287     * If user clicked with the left button, delete the nearest object.
288     */
289    @Override
290    public void mouseReleased(MouseEvent e) {
291        if (e.getButton() != MouseEvent.BUTTON1)
292            return;
293        MapFrame map = MainApplication.getMap();
294        if (!map.mapView.isActiveLayerVisible())
295            return;
296
297        // request focus in order to enable the expected keyboard shortcuts
298        //
299        map.mapView.requestFocus();
300
301        Command c = buildDeleteCommands(e, e.getModifiersEx(), false);
302        if (c != null) {
303            UndoRedoHandler.getInstance().add(c);
304        }
305
306        getLayerManager().getEditDataSet().setSelected();
307        giveUserFeedback(e);
308    }
309
310    @Override
311    public String getModeHelpText() {
312        // CHECKSTYLE.OFF: LineLength
313        return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
314        // CHECKSTYLE.ON: LineLength
315    }
316
317    @Override
318    public boolean layerIsSupported(Layer l) {
319        return isEditableDataLayer(l);
320    }
321
322    @Override
323    protected void updateEnabledState() {
324        setEnabled(MainApplication.isDisplayingMapView() && MainApplication.getMap().mapView.isActiveLayerDrawable());
325    }
326
327    /**
328     * Deletes the relation in the context of the given layer.
329     *
330     * @param layer the layer in whose context the relation is deleted. Must not be null.
331     * @param toDelete  the relation to be deleted. Must not be null.
332     * @throws IllegalArgumentException if layer is null
333     * @throws IllegalArgumentException if toDelete is null
334     */
335    public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
336        deleteRelations(layer, Collections.singleton(toDelete));
337    }
338
339    /**
340     * Deletes the relations in the context of the given layer.
341     *
342     * @param layer the layer in whose context the relations are deleted. Must not be null.
343     * @param toDelete the relations to be deleted. Must not be null.
344     * @throws IllegalArgumentException if layer is null
345     * @throws IllegalArgumentException if toDelete is null
346     */
347    public static void deleteRelations(OsmDataLayer layer, Collection<Relation> toDelete) {
348        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
349        CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
350
351        final Command cmd = DeleteCommand.delete(toDelete);
352        if (cmd != null) {
353            // cmd can be null if the user cancels dialogs DialogCommand displays
354            List<Relation> toUnselect = toDelete.stream().filter(Relation::isSelected).collect(Collectors.toList());
355            UndoRedoHandler.getInstance().add(cmd);
356            toDelete.forEach(relation -> RelationDialogManager.getRelationDialogManager().close(layer, relation));
357            toUnselect.forEach(layer.data::toggleSelected);
358        }
359    }
360
361    private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
362        updateKeyModifiersEx(modifiers);
363
364        DeleteParameters result = new DeleteParameters();
365
366        MapView mapView = MainApplication.getMap().mapView;
367        result.nearestNode = mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
368        if (result.nearestNode == null) {
369            result.nearestSegment = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
370            if (result.nearestSegment != null) {
371                if (shift) {
372                    result.mode = DeleteMode.segment;
373                } else if (ctrl) {
374                    result.mode = DeleteMode.way_with_references;
375                } else {
376                    result.mode = alt ? DeleteMode.way : DeleteMode.way_with_nodes;
377                }
378            } else {
379                result.mode = DeleteMode.none;
380            }
381        } else if (ctrl) {
382            result.mode = DeleteMode.node_with_references;
383        } else {
384            result.mode = DeleteMode.node;
385        }
386
387        return result;
388    }
389
390    /**
391     * This function takes any mouse event argument and builds the list of elements
392     * that should be deleted but does not actually delete them.
393     * @param e MouseEvent from which modifiers and position are taken
394     * @param modifiers For explanation, see {@link #updateCursor}
395     * @param silent Set to true if the user should not be bugged with additional dialogs
396     * @return delete command
397     */
398    private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
399        DeleteParameters parameters = getDeleteParameters(e, modifiers);
400        switch (parameters.mode) {
401        case node:
402            return DeleteCommand.delete(Collections.singleton(parameters.nearestNode), false, silent);
403        case node_with_references:
404            return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestNode), silent);
405        case segment:
406            return DeleteCommand.deleteWaySegment(parameters.nearestSegment);
407        case way:
408            return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), false, silent);
409        case way_with_nodes:
410            return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), true, silent);
411        case way_with_references:
412            return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestSegment.way), true);
413        default:
414            return null;
415        }
416    }
417
418    /**
419     * This is required to update the cursors when ctrl/shift/alt is pressed
420     */
421    @Override
422    public void modifiersExChanged(int modifiers) {
423        if (oldEvent == null)
424            return;
425        // We don't have a mouse event, so we pass the old mouse event but the new modifiers.
426        giveUserFeedback(oldEvent, modifiers);
427    }
428}