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}