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 }