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