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