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