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
038 * 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
050    // pressed but the mouse isn't moved)
051    private MouseEvent oldEvent = null;
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 WaySegment oldHighlightedWaySegment = null;
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        private 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        DeleteMode mode;
085        Node nearestNode;
086        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        if (!Main.map.mapView.isActiveLayerDrawable())
137            return;
138        boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
139        boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
140
141        Command c;
142        if (ctrl) {
143            c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected());
144        } else {
145            c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */);
146        }
147        // if c is null, an error occurred or the user aborted. Don't do anything in that case.
148        if (c != null) {
149            Main.main.undoRedo.add(c);
150            getCurrentDataSet().setSelected();
151            Main.map.repaint();
152        }
153    }
154
155    @Override
156    public void mouseDragged(MouseEvent e) {
157        mouseMoved(e);
158    }
159
160    /**
161     * Listen to mouse move to be able to update the cursor (and highlights)
162     * @param e The mouse event that has been captured
163     */
164    @Override
165    public void mouseMoved(MouseEvent e) {
166        oldEvent = e;
167        giveUserFeedback(e);
168    }
169
170    /**
171     * removes any highlighting that may have been set beforehand.
172     */
173    private void removeHighlighting() {
174        highlightHelper.clear();
175        DataSet ds = getCurrentDataSet();
176        if(ds != null) {
177            ds.clearHighlightedWaySegments();
178        }
179    }
180
181    /**
182     * handles everything related to highlighting primitives and way
183     * segments for the given pointer position (via MouseEvent) and
184     * modifiers.
185     * @param e
186     * @param modifiers
187     */
188    private void addHighlighting(MouseEvent e, int modifiers) {
189        if(!drawTargetHighlight)
190            return;
191
192        Set<OsmPrimitive> newHighlights = new HashSet<>();
193        DeleteParameters parameters = getDeleteParameters(e, modifiers);
194
195        if(parameters.mode == DeleteMode.segment) {
196            // deleting segments is the only action not working on OsmPrimitives
197            // so we have to handle them separately.
198            repaintIfRequired(newHighlights, parameters.nearestSegment);
199        } else {
200            // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support
201            // silent operation and SplitWayAction will show dialogs. A lot.
202            Command delCmd = buildDeleteCommands(e, modifiers, true);
203            if(delCmd != null) {
204                // all other cases delete OsmPrimitives directly, so we can
205                // safely do the following
206                for(OsmPrimitive osm : delCmd.getParticipatingPrimitives()) {
207                    newHighlights.add(osm);
208                }
209            }
210            repaintIfRequired(newHighlights, null);
211        }
212    }
213
214    private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
215        boolean needsRepaint = false;
216        DataSet ds = getCurrentDataSet();
217
218        if(newHighlightedWaySegment == null && oldHighlightedWaySegment != null) {
219            if(ds != null) {
220                ds.clearHighlightedWaySegments();
221                needsRepaint = true;
222            }
223            oldHighlightedWaySegment = null;
224        } else if(newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) {
225            if(ds != null) {
226                ds.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment));
227                needsRepaint = true;
228            }
229            oldHighlightedWaySegment = newHighlightedWaySegment;
230        }
231        needsRepaint |= highlightHelper.highlightOnly(newHighlights);
232        if(needsRepaint) {
233            Main.map.mapView.repaint();
234        }
235    }
236
237    /**
238     * This function handles all work related to updating the cursor and
239     * highlights
240     *
241     * @param e
242     * @param modifiers
243     */
244    private void updateCursor(MouseEvent e, int modifiers) {
245        if (!Main.isDisplayingMapView())
246            return;
247        if(!Main.map.mapView.isActiveLayerVisible() || e == null)
248            return;
249
250        DeleteParameters parameters = getDeleteParameters(e, modifiers);
251        Main.map.mapView.setNewCursor(parameters.mode.cursor(), this);
252    }
253
254    /**
255     * Gives the user feedback for the action he/she is about to do. Currently
256     * calls the cursor and target highlighting routines. Allows for modifiers
257     * not taken from the given mouse event.
258     *
259     * Normally the mouse event also contains the modifiers. However, when the
260     * mouse is not moved and only modifier keys are pressed, no mouse event
261     * occurs. We can use AWTEvent to catch those but still lack a proper
262     * mouseevent. Instead we copy the previous event and only update the
263     * modifiers.
264     */
265    private void giveUserFeedback(MouseEvent e, int modifiers) {
266        updateCursor(e, modifiers);
267        addHighlighting(e, modifiers);
268    }
269
270    /**
271     * Gives the user feedback for the action he/she is about to do. Currently
272     * calls the cursor and target highlighting routines. Extracts modifiers
273     * from mouse event.
274     */
275    private void giveUserFeedback(MouseEvent e) {
276        giveUserFeedback(e, e.getModifiers());
277    }
278
279    /**
280     * If user clicked with the left button, delete the nearest object.
281     * position.
282     */
283    @Override
284    public void mouseReleased(MouseEvent e) {
285        if (e.getButton() != MouseEvent.BUTTON1)
286            return;
287        if(!Main.map.mapView.isActiveLayerVisible())
288            return;
289
290        // request focus in order to enable the expected keyboard shortcuts
291        //
292        Main.map.mapView.requestFocus();
293
294        Command c = buildDeleteCommands(e, e.getModifiers(), false);
295        if (c != null) {
296            Main.main.undoRedo.add(c);
297        }
298
299        getCurrentDataSet().setSelected();
300        giveUserFeedback(e);
301    }
302
303    @Override
304    public String getModeHelpText() {
305        return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
306    }
307
308    @Override
309    public boolean layerIsSupported(Layer l) {
310        return l instanceof OsmDataLayer;
311    }
312
313    @Override
314    protected void updateEnabledState() {
315        setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable());
316    }
317
318    /**
319     * Deletes the relation in the context of the given layer.
320     *
321     * @param layer the layer in whose context the relation is deleted. Must not be null.
322     * @param toDelete  the relation to be deleted. Must  not be null.
323     * @exception IllegalArgumentException thrown if layer is null
324     * @exception IllegalArgumentException thrown if toDelete is nul
325     */
326    public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
327        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
328        CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
329
330        Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete));
331        if (cmd != null) {
332            // cmd can be null if the user cancels dialogs DialogCommand displays
333            Main.main.undoRedo.add(cmd);
334            if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) {
335                getCurrentDataSet().toggleSelected(toDelete);
336            }
337            RelationDialogManager.getRelationDialogManager().close(layer, toDelete);
338        }
339    }
340
341    private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
342        updateKeyModifiers(modifiers);
343
344        DeleteParameters result = new DeleteParameters();
345
346        result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate);
347        if (result.nearestNode == null) {
348            result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
349            if (result.nearestSegment != null) {
350                if (shift) {
351                    result.mode = DeleteMode.segment;
352                } else if (ctrl) {
353                    result.mode = DeleteMode.way_with_references;
354                } else {
355                    result.mode = alt?DeleteMode.way:DeleteMode.way_with_nodes;
356                }
357            } else {
358                result.mode = DeleteMode.none;
359            }
360        } else if (ctrl) {
361            result.mode = DeleteMode.node_with_references;
362        } else {
363            result.mode = DeleteMode.node;
364        }
365
366        return result;
367    }
368
369    /**
370     * This function takes any mouse event argument and builds the list of elements
371     * that should be deleted but does not actually delete them.
372     * @param e MouseEvent from which modifiers and position are taken
373     * @param modifiers For explanation, see {@link #updateCursor}
374     * @param silent Set to true if the user should not be bugged with additional
375     *        dialogs
376     * @return delete command
377     */
378    private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
379        DeleteParameters parameters = getDeleteParameters(e, modifiers);
380        switch (parameters.mode) {
381        case node:
382            return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent);
383        case node_with_references:
384            return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode), silent);
385        case segment:
386            return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment);
387        case way:
388            return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent);
389        case way_with_nodes:
390            return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent);
391        case way_with_references:
392            return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true);
393        default:
394            return null;
395        }
396    }
397
398    /**
399     * This is required to update the cursors when ctrl/shift/alt is pressed
400     */
401    @Override
402    public void modifiersChanged(int modifiers) {
403        if (oldEvent == null)
404            return;
405        // We don't have a mouse event, so we pass the old mouse event but the new modifiers.
406        giveUserFeedback(oldEvent, modifiers);
407    }
408}