001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.actions;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Component;
007    import java.awt.Dimension;
008    import java.awt.GridBagLayout;
009    import java.awt.Insets;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.KeyEvent;
012    import java.util.ArrayList;
013    import java.util.Collection;
014    import java.util.Collections;
015    import java.util.Comparator;
016    import java.util.HashSet;
017    import java.util.List;
018    import java.util.Set;
019    
020    import javax.swing.AbstractAction;
021    import javax.swing.BorderFactory;
022    import javax.swing.Box;
023    import javax.swing.JButton;
024    import javax.swing.JCheckBox;
025    import javax.swing.JLabel;
026    import javax.swing.JList;
027    import javax.swing.JPanel;
028    import javax.swing.JScrollPane;
029    import javax.swing.JSeparator;
030    
031    import org.openstreetmap.josm.Main;
032    import org.openstreetmap.josm.command.PurgeCommand;
033    import org.openstreetmap.josm.data.osm.Node;
034    import org.openstreetmap.josm.data.osm.OsmPrimitive;
035    import org.openstreetmap.josm.data.osm.Relation;
036    import org.openstreetmap.josm.data.osm.RelationMember;
037    import org.openstreetmap.josm.data.osm.Way;
038    import org.openstreetmap.josm.gui.ExtendedDialog;
039    import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
040    import org.openstreetmap.josm.gui.help.HelpUtil;
041    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
042    import org.openstreetmap.josm.tools.GBC;
043    import org.openstreetmap.josm.tools.ImageProvider;
044    import 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     */
057    public 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.map.mapView.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            while (!toPurge.isEmpty()) {
101                OsmPrimitive osm = toPurge.iterator().next();
102                for (OsmPrimitive parent: osm.getReferrers()) {
103                    if (toPurge.contains(parent) || toPurgeChecked.contains(parent)) {
104                        continue;
105                    }
106                    if (parent instanceof Way || (parent instanceof Relation && osm.isNew())) {
107                        toPurgeAdditionally.add(parent);
108                        toPurge.add(parent);
109                    }
110                }
111                toPurge.remove(osm);
112                toPurgeChecked.add(osm);
113            }
114    
115            makeIncomplete = new HashSet<OsmPrimitive>();
116    
117            // Find the objects that will be incomplete after purging.
118            // At this point, all parents of new to-be-purged primitives are
119            // also to-be-purged and
120            // all parents of not-new to-be-purged primitives are either
121            // to-be-purged or of type relation.
122            TOP:
123                for (OsmPrimitive child : toPurgeChecked) {
124                    if (child.isNew()) {
125                        continue;
126                    }
127                    for (OsmPrimitive parent : child.getReferrers()) {
128                        if (parent instanceof Relation && !toPurgeChecked.contains(parent)) {
129                            makeIncomplete.add(child);
130                            continue TOP;
131                        }
132                    }
133                }
134    
135            // Add untagged way nodes. Do not add nodes that have other
136            // referrers not yet to-be-purged.
137            if (Main.pref.getBoolean("purge.add_untagged_waynodes", true)) {
138                Set<OsmPrimitive> wayNodes = new HashSet<OsmPrimitive>();
139                for (OsmPrimitive osm : toPurgeChecked) {
140                    if (osm instanceof Way) {
141                        Way w = (Way) osm;
142                        NODE:
143                            for (Node n : w.getNodes()) {
144                                if (n.isTagged() || toPurgeChecked.contains(n)) {
145                                    continue;
146                                }
147                                for (OsmPrimitive ref : n.getReferrers()) {
148                                    if (ref != w && !toPurgeChecked.contains(ref)) {
149                                        continue NODE;
150                                    }
151                                }
152                                wayNodes.add(n);
153                            }
154                    }
155                }
156                toPurgeChecked.addAll(wayNodes);
157                toPurgeAdditionally.addAll(wayNodes);
158            }
159    
160            if (Main.pref.getBoolean("purge.add_relations_with_only_incomplete_members", true)) {
161                Set<Relation> relSet = new HashSet<Relation>();
162                for (OsmPrimitive osm : toPurgeChecked) {
163                    for (OsmPrimitive parent : osm.getReferrers()) {
164                        if (parent instanceof Relation
165                                && !(toPurgeChecked.contains(parent))
166                                && hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relSet)) {
167                            relSet.add((Relation) parent);
168                        }
169                    }
170                }
171    
172                /**
173                 * Add higher level relations (list gets extended while looping over it)
174                 */
175                List<Relation> relLst = new ArrayList<Relation>(relSet);
176                for (int i=0; i<relLst.size(); ++i) {
177                    for (OsmPrimitive parent : relLst.get(i).getReferrers()) {
178                        if (!(toPurgeChecked.contains(parent))
179                                && hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relLst)) {
180                            relLst.add((Relation) parent);
181                        }
182                    }
183                }
184                relSet = new HashSet<Relation>(relLst);
185                toPurgeChecked.addAll(relSet);
186                toPurgeAdditionally.addAll(relSet);
187            }
188    
189            boolean modified = false;
190            for (OsmPrimitive osm : toPurgeChecked) {
191                if (osm.isModified()) {
192                    modified = true;
193                    break;
194                }
195            }
196    
197            ExtendedDialog confirmDlg = new ExtendedDialog(Main.parent, tr("Confirm Purging"), new String[] {tr("Purge"), tr("Cancel")});
198            confirmDlg.setContent(buildPanel(modified), false);
199            confirmDlg.setButtonIcons(new String[] {"ok", "cancel"});
200    
201            int answer = confirmDlg.showDialog().getValue();
202            if (answer != 1)
203                return;
204    
205            Main.pref.put("purge.clear_undo_redo", cbClearUndoRedo.isSelected());
206    
207            Main.main.undoRedo.add(new PurgeCommand(Main.map.mapView.getEditLayer(), toPurgeChecked, makeIncomplete));
208    
209            if (cbClearUndoRedo.isSelected()) {
210                Main.main.undoRedo.clean();
211                getCurrentDataSet().clearSelectionHistory();
212            }
213        }
214    
215        private JPanel buildPanel(boolean modified) {
216            JPanel pnl = new JPanel(new GridBagLayout());
217    
218            pnl.add(Box.createRigidArea(new Dimension(400,0)), GBC.eol().fill(GBC.HORIZONTAL));
219    
220            pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
221            pnl.add(new JLabel("<html>"+
222                    tr("This operation makes JOSM forget the selected objects.<br> " +
223                            "They will be removed from the layer, but <i>not</i> deleted<br> " +
224                            "on the server when uploading.")+"</html>",
225                            ImageProvider.get("purge"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
226    
227            if (!toPurgeAdditionally.isEmpty()) {
228                pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0,5,0,5));
229                pnl.add(new JLabel("<html>"+
230                        tr("The following dependent objects will be purged<br> " +
231                                "in addition to the selected objects:")+"</html>",
232                                ImageProvider.get("warning-small"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
233    
234                Collections.sort(toPurgeAdditionally, new Comparator<OsmPrimitive>() {
235                    public int compare(OsmPrimitive o1, OsmPrimitive o2) {
236                        int type = o2.getType().compareTo(o1.getType());
237                        if (type != 0)
238                            return type;
239                        return (Long.valueOf(o1.getUniqueId())).compareTo(o2.getUniqueId());
240                    }
241                });
242                JList list = new JList(toPurgeAdditionally.toArray(new OsmPrimitive[0]));
243                /* force selection to be active for all entries */
244                list.setCellRenderer(new OsmPrimitivRenderer() {
245                    @Override
246                    public Component getListCellRendererComponent(JList list,
247                            Object value,
248                            int index,
249                            boolean isSelected,
250                            boolean cellHasFocus) {
251                        return super.getListCellRendererComponent(list, value, index, true, false);
252                    }
253                });
254                JScrollPane scroll = new JScrollPane(list);
255                scroll.setPreferredSize(new Dimension(250, 300));
256                scroll.setMinimumSize(new Dimension(250, 300));
257                pnl.add(scroll, GBC.std().fill(GBC.VERTICAL).weight(0.0, 1.0));
258    
259                JButton addToSelection = new JButton(new AbstractAction() {
260                    {
261                        putValue(SHORT_DESCRIPTION,   tr("Add to selection"));
262                        putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
263                    }
264    
265                    public void actionPerformed(ActionEvent e) {
266                        layer.data.addSelected(toPurgeAdditionally);
267                    }
268                });
269                addToSelection.setMargin(new Insets(0,0,0,0));
270                pnl.add(addToSelection, GBC.eol().anchor(GBC.SOUTHWEST).weight(1.0, 1.0).insets(2,0,0,3));
271            }
272    
273            if (modified) {
274                pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0,5,0,5));
275                pnl.add(new JLabel("<html>"+tr("Some of the objects are modified.<br> " +
276                        "Proceed, if these changes should be discarded."+"</html>"),
277                        ImageProvider.get("warning-small"), JLabel.LEFT),
278                        GBC.eol().fill(GBC.HORIZONTAL));
279            }
280    
281            cbClearUndoRedo = new JCheckBox(tr("Clear Undo/Redo buffer"));
282            cbClearUndoRedo.setSelected(Main.pref.getBoolean("purge.clear_undo_redo", false));
283    
284            pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0,5,0,5));
285            pnl.add(cbClearUndoRedo, GBC.eol());
286            return pnl;
287        }
288    
289        @Override
290        protected void updateEnabledState() {
291            if (getCurrentDataSet() == null) {
292                setEnabled(false);
293            } else {
294                setEnabled(!(getCurrentDataSet().selectionEmpty()));
295            }
296        }
297    
298        @Override
299        protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
300            setEnabled(selection != null && !selection.isEmpty());
301        }
302    
303        private boolean hasOnlyIncompleteMembers(Relation r, Collection<OsmPrimitive> toPurge, Collection<? extends OsmPrimitive> moreToPurge) {
304            for (RelationMember m : r.getMembers()) {
305                if (!m.getMember().isIncomplete() && !toPurge.contains(m.getMember()) && !moreToPurge.contains(m.getMember()))
306                    return false;
307            }
308            return true;
309        }
310    }