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