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