001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Dimension;
008import java.awt.GraphicsEnvironment;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ActionEvent;
012import java.awt.event.KeyEvent;
013import java.util.ArrayList;
014import java.util.Collection;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Set;
018
019import javax.swing.AbstractAction;
020import javax.swing.BorderFactory;
021import javax.swing.Box;
022import javax.swing.JButton;
023import javax.swing.JCheckBox;
024import javax.swing.JLabel;
025import javax.swing.JList;
026import javax.swing.JOptionPane;
027import javax.swing.JPanel;
028import javax.swing.JScrollPane;
029import javax.swing.JSeparator;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.command.PurgeCommand;
033import org.openstreetmap.josm.data.osm.DataSet;
034import org.openstreetmap.josm.data.osm.Node;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.Relation;
037import org.openstreetmap.josm.data.osm.RelationMember;
038import org.openstreetmap.josm.data.osm.Way;
039import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
040import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
041import org.openstreetmap.josm.gui.help.HelpUtil;
042import org.openstreetmap.josm.gui.layer.OsmDataLayer;
043import org.openstreetmap.josm.tools.GBC;
044import org.openstreetmap.josm.tools.ImageProvider;
045import org.openstreetmap.josm.tools.Shortcut;
046
047/**
048 * The action to purge the selected primitives, i.e. remove them from the
049 * data layer, or remove their content and make them incomplete.
050 *
051 * This means, the deleted flag is not affected and JOSM simply forgets
052 * about these primitives.
053 *
054 * This action is undo-able. In order not to break previous commands in the
055 * undo buffer, we must re-add the identical object (and not semantically
056 * equal ones).
057 */
058public class PurgeAction extends JosmAction {
059
060    protected transient OsmDataLayer layer;
061    protected JCheckBox cbClearUndoRedo;
062    protected boolean modified;
063
064    protected transient Set<OsmPrimitive> toPurge;
065    /**
066     * finally, contains all objects that are purged
067     */
068    protected transient Set<OsmPrimitive> toPurgeChecked;
069    /**
070     * Subset of toPurgeChecked. Marks primitives that remain in the
071     * dataset, but incomplete.
072     */
073    protected transient Set<OsmPrimitive> makeIncomplete;
074    /**
075     * Subset of toPurgeChecked. Those that have not been in the selection.
076     */
077    protected transient List<OsmPrimitive> toPurgeAdditionally;
078
079    /**
080     * Constructs a new {@code PurgeAction}.
081     */
082    public PurgeAction() {
083        /* translator note: other expressions for "purge" might be "forget", "clean", "obliterate", "prune" */
084        super(tr("Purge..."), "purge", tr("Forget objects but do not delete them on server when uploading."),
085                Shortcut.registerShortcut("system:purge", tr("Edit: {0}", tr("Purge")),
086                KeyEvent.VK_P, Shortcut.CTRL_SHIFT),
087                true);
088        putValue("help", HelpUtil.ht("/Action/Purge"));
089    }
090
091    /** force selection to be active for all entries */
092    static class SelectionForcedOsmPrimitivRenderer extends OsmPrimitivRenderer {
093        @Override
094        public Component getListCellRendererComponent(JList<? extends OsmPrimitive> list,
095                OsmPrimitive value, int index, boolean isSelected, boolean cellHasFocus) {
096            return super.getListCellRendererComponent(list, value, index, true, false);
097        }
098    }
099
100    @Override
101    public void actionPerformed(ActionEvent e) {
102        if (!isEnabled())
103            return;
104
105        PurgeCommand cmd = getPurgeCommand(getLayerManager().getEditDataSet().getAllSelected());
106        boolean clearUndoRedo = false;
107
108        if (!GraphicsEnvironment.isHeadless()) {
109            final boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
110                    "purge", Main.parent, buildPanel(modified), tr("Confirm Purging"),
111                    JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_OPTION);
112            if (!answer)
113                return;
114
115            clearUndoRedo = cbClearUndoRedo.isSelected();
116            Main.pref.put("purge.clear_undo_redo", clearUndoRedo);
117        }
118
119        Main.main.undoRedo.add(cmd);
120        if (clearUndoRedo) {
121            Main.main.undoRedo.clean();
122            getLayerManager().getEditDataSet().clearSelectionHistory();
123        }
124    }
125
126    /**
127     * Creates command to purge selected OSM primitives.
128     * @param sel selected OSM primitives
129     * @return command to purge selected OSM primitives
130     * @since 11252
131     */
132    public PurgeCommand getPurgeCommand(Collection<OsmPrimitive> sel) {
133        layer = Main.getLayerManager().getEditLayer();
134
135        toPurge = new HashSet<>(sel);
136        toPurgeAdditionally = new ArrayList<>();
137        toPurgeChecked = new HashSet<>();
138
139        // Add referrer, unless the object to purge is not new and the parent is a relation
140        Set<OsmPrimitive> toPurgeRecursive = new HashSet<>();
141        while (!toPurge.isEmpty()) {
142
143            for (OsmPrimitive osm: toPurge) {
144                for (OsmPrimitive parent: osm.getReferrers()) {
145                    if (toPurge.contains(parent) || toPurgeChecked.contains(parent) || toPurgeRecursive.contains(parent)) {
146                        continue;
147                    }
148                    if (parent instanceof Way || (parent instanceof Relation && osm.isNew())) {
149                        toPurgeAdditionally.add(parent);
150                        toPurgeRecursive.add(parent);
151                    }
152                }
153                toPurgeChecked.add(osm);
154            }
155            toPurge = toPurgeRecursive;
156            toPurgeRecursive = new HashSet<>();
157        }
158
159        makeIncomplete = new HashSet<>();
160
161        // Find the objects that will be incomplete after purging.
162        // At this point, all parents of new to-be-purged primitives are
163        // also to-be-purged and
164        // all parents of not-new to-be-purged primitives are either
165        // to-be-purged or of type relation.
166        TOP:
167            for (OsmPrimitive child : toPurgeChecked) {
168                if (child.isNew()) {
169                    continue;
170                }
171                for (OsmPrimitive parent : child.getReferrers()) {
172                    if (parent instanceof Relation && !toPurgeChecked.contains(parent)) {
173                        makeIncomplete.add(child);
174                        continue TOP;
175                    }
176                }
177            }
178
179        // Add untagged way nodes. Do not add nodes that have other referrers not yet to-be-purged.
180        if (Main.pref.getBoolean("purge.add_untagged_waynodes", true)) {
181            Set<OsmPrimitive> wayNodes = new HashSet<>();
182            for (OsmPrimitive osm : toPurgeChecked) {
183                if (osm instanceof Way) {
184                    Way w = (Way) osm;
185                    NODE:
186                        for (Node n : w.getNodes()) {
187                            if (n.isTagged() || toPurgeChecked.contains(n)) {
188                                continue;
189                            }
190                            for (OsmPrimitive ref : n.getReferrers()) {
191                                if (ref != w && !toPurgeChecked.contains(ref)) {
192                                    continue NODE;
193                                }
194                            }
195                            wayNodes.add(n);
196                        }
197                }
198            }
199            toPurgeChecked.addAll(wayNodes);
200            toPurgeAdditionally.addAll(wayNodes);
201        }
202
203        if (Main.pref.getBoolean("purge.add_relations_with_only_incomplete_members", true)) {
204            Set<Relation> relSet = new HashSet<>();
205            for (OsmPrimitive osm : toPurgeChecked) {
206                for (OsmPrimitive parent : osm.getReferrers()) {
207                    if (parent instanceof Relation
208                            && !(toPurgeChecked.contains(parent))
209                            && hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relSet)) {
210                        relSet.add((Relation) parent);
211                    }
212                }
213            }
214
215            // Add higher level relations (list gets extended while looping over it)
216            List<Relation> relLst = new ArrayList<>(relSet);
217            for (int i = 0; i < relLst.size(); ++i) { // foreach loop not applicable since list gets extended while looping over it
218                for (OsmPrimitive parent : relLst.get(i).getReferrers()) {
219                    if (!(toPurgeChecked.contains(parent))
220                            && hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relLst)) {
221                        relLst.add((Relation) parent);
222                    }
223                }
224            }
225            relSet = new HashSet<>(relLst);
226            toPurgeChecked.addAll(relSet);
227            toPurgeAdditionally.addAll(relSet);
228        }
229
230        modified = false;
231        for (OsmPrimitive osm : toPurgeChecked) {
232            if (osm.isModified()) {
233                modified = true;
234                break;
235            }
236        }
237
238        return layer != null ? new PurgeCommand(layer, toPurgeChecked, makeIncomplete) :
239            new PurgeCommand(toPurgeChecked.iterator().next().getDataSet(), toPurgeChecked, makeIncomplete);
240    }
241
242    private JPanel buildPanel(boolean modified) {
243        JPanel pnl = new JPanel(new GridBagLayout());
244
245        pnl.add(Box.createRigidArea(new Dimension(400, 0)), GBC.eol().fill(GBC.HORIZONTAL));
246
247        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
248        pnl.add(new JLabel("<html>"+
249                tr("This operation makes JOSM forget the selected objects.<br> " +
250                        "They will be removed from the layer, but <i>not</i> deleted<br> " +
251                        "on the server when uploading.")+"</html>",
252                        ImageProvider.get("purge"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
253
254        if (!toPurgeAdditionally.isEmpty()) {
255            pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
256            pnl.add(new JLabel("<html>"+
257                    tr("The following dependent objects will be purged<br> " +
258                            "in addition to the selected objects:")+"</html>",
259                            ImageProvider.get("warning-small"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
260
261            toPurgeAdditionally.sort((o1, o2) -> {
262                int type = o2.getType().compareTo(o1.getType());
263                if (type != 0)
264                    return type;
265                return Long.compare(o1.getUniqueId(), o2.getUniqueId());
266            });
267            JList<OsmPrimitive> list = new JList<>(toPurgeAdditionally.toArray(new OsmPrimitive[toPurgeAdditionally.size()]));
268            /* force selection to be active for all entries */
269            list.setCellRenderer(new SelectionForcedOsmPrimitivRenderer());
270            JScrollPane scroll = new JScrollPane(list);
271            scroll.setPreferredSize(new Dimension(250, 300));
272            scroll.setMinimumSize(new Dimension(250, 300));
273            pnl.add(scroll, GBC.std().fill(GBC.BOTH).weight(1.0, 1.0));
274
275            JButton addToSelection = new JButton(new AbstractAction() {
276                {
277                    putValue(SHORT_DESCRIPTION, tr("Add to selection"));
278                    putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
279                }
280
281                @Override
282                public void actionPerformed(ActionEvent e) {
283                    layer.data.addSelected(toPurgeAdditionally);
284                }
285            });
286            addToSelection.setMargin(new Insets(0, 0, 0, 0));
287            pnl.add(addToSelection, GBC.eol().anchor(GBC.SOUTHWEST).weight(0.0, 1.0).insets(2, 0, 0, 3));
288        }
289
290        if (modified) {
291            pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
292            pnl.add(new JLabel("<html>"+tr("Some of the objects are modified.<br> " +
293                    "Proceed, if these changes should be discarded."+"</html>"),
294                    ImageProvider.get("warning-small"), JLabel.LEFT),
295                    GBC.eol().fill(GBC.HORIZONTAL));
296        }
297
298        cbClearUndoRedo = new JCheckBox(tr("Clear Undo/Redo buffer"));
299        cbClearUndoRedo.setSelected(Main.pref.getBoolean("purge.clear_undo_redo", false));
300
301        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
302        pnl.add(cbClearUndoRedo, GBC.eol());
303        return pnl;
304    }
305
306    @Override
307    protected void updateEnabledState() {
308        DataSet ds = getLayerManager().getEditDataSet();
309        if (ds == null) {
310            setEnabled(false);
311        } else {
312            setEnabled(!ds.selectionEmpty());
313        }
314    }
315
316    @Override
317    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
318        setEnabled(selection != null && !selection.isEmpty());
319    }
320
321    private static boolean hasOnlyIncompleteMembers(
322            Relation r, Collection<OsmPrimitive> toPurge, Collection<? extends OsmPrimitive> moreToPurge) {
323        for (RelationMember m : r.getMembers()) {
324            if (!m.getMember().isIncomplete() && !toPurge.contains(m.getMember()) && !moreToPurge.contains(m.getMember()))
325                return false;
326        }
327        return true;
328    }
329}