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