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