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