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