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