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     * @param e mouse event
259     * @param modifiers mouse modifiers
260     */
261    private void giveUserFeedback(MouseEvent e, int modifiers) {
262        updateCursor(e, modifiers);
263        addHighlighting(e, modifiers);
264    }
265
266    /**
267     * Gives the user feedback for the action he/she is about to do. Currently
268     * calls the cursor and target highlighting routines. Extracts modifiers
269     * from mouse event.
270     * @param e mouse event
271     */
272    private void giveUserFeedback(MouseEvent e) {
273        giveUserFeedback(e, e.getModifiers());
274    }
275
276    /**
277     * If user clicked with the left button, delete the nearest object.
278     */
279    @Override
280    public void mouseReleased(MouseEvent e) {
281        if (e.getButton() != MouseEvent.BUTTON1)
282            return;
283        if (!Main.map.mapView.isActiveLayerVisible())
284            return;
285
286        // request focus in order to enable the expected keyboard shortcuts
287        //
288        Main.map.mapView.requestFocus();
289
290        Command c = buildDeleteCommands(e, e.getModifiers(), false);
291        if (c != null) {
292            Main.main.undoRedo.add(c);
293        }
294
295        getCurrentDataSet().setSelected();
296        giveUserFeedback(e);
297    }
298
299    @Override
300    public String getModeHelpText() {
301        // CHECKSTYLE.OFF: LineLength
302        return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
303        // CHECKSTYLE.ON: LineLength
304    }
305
306    @Override
307    public boolean layerIsSupported(Layer l) {
308        return l instanceof OsmDataLayer;
309    }
310
311    @Override
312    protected void updateEnabledState() {
313        setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable());
314    }
315
316    /**
317     * Deletes the relation in the context of the given layer.
318     *
319     * @param layer the layer in whose context the relation is deleted. Must not be null.
320     * @param toDelete  the relation to be deleted. Must  not be null.
321     * @throws IllegalArgumentException if layer is null
322     * @throws IllegalArgumentException if toDelete is nul
323     */
324    public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
325        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
326        CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
327
328        Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete));
329        if (cmd != null) {
330            // cmd can be null if the user cancels dialogs DialogCommand displays
331            Main.main.undoRedo.add(cmd);
332            if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) {
333                getCurrentDataSet().toggleSelected(toDelete);
334            }
335            RelationDialogManager.getRelationDialogManager().close(layer, toDelete);
336        }
337    }
338
339    private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
340        updateKeyModifiers(modifiers);
341
342        DeleteParameters result = new DeleteParameters();
343
344        result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate);
345        if (result.nearestNode == null) {
346            result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
347            if (result.nearestSegment != null) {
348                if (shift) {
349                    result.mode = DeleteMode.segment;
350                } else if (ctrl) {
351                    result.mode = DeleteMode.way_with_references;
352                } else {
353                    result.mode = alt ? DeleteMode.way : DeleteMode.way_with_nodes;
354                }
355            } else {
356                result.mode = DeleteMode.none;
357            }
358        } else if (ctrl) {
359            result.mode = DeleteMode.node_with_references;
360        } else {
361            result.mode = DeleteMode.node;
362        }
363
364        return result;
365    }
366
367    /**
368     * This function takes any mouse event argument and builds the list of elements
369     * that should be deleted but does not actually delete them.
370     * @param e MouseEvent from which modifiers and position are taken
371     * @param modifiers For explanation, see {@link #updateCursor}
372     * @param silent Set to true if the user should not be bugged with additional dialogs
373     * @return delete command
374     */
375    private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
376        DeleteParameters parameters = getDeleteParameters(e, modifiers);
377        switch (parameters.mode) {
378        case node:
379            return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestNode), false, silent);
380        case node_with_references:
381            return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestNode), silent);
382        case segment:
383            return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment);
384        case way:
385            return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent);
386        case way_with_nodes:
387            return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent);
388        case way_with_references:
389            return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true);
390        default:
391            return null;
392        }
393    }
394
395    /**
396     * This is required to update the cursors when ctrl/shift/alt is pressed
397     */
398    @Override
399    public void modifiersChanged(int modifiers) {
400        if (oldEvent == null)
401            return;
402        // We don't have a mouse event, so we pass the old mouse event but the new modifiers.
403        giveUserFeedback(oldEvent, modifiers);
404    }
405}