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.HashSet; 016import java.util.List; 017import java.util.Set; 018 019import javax.swing.AbstractAction; 020import javax.swing.BorderFactory; 021import javax.swing.Box; 022import javax.swing.JButton; 023import javax.swing.JCheckBox; 024import javax.swing.JLabel; 025import javax.swing.JList; 026import javax.swing.JOptionPane; 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.DataSet; 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.ConditionalOptionPaneUtil; 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 = getLayerManager().getEditDataSet().getAllSelected(); 096 layer = Main.getLayerManager().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 final boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 209 "purge", Main.parent, buildPanel(modified), tr("Confirm Purging"), 210 JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_OPTION); 211 if (!answer) 212 return; 213 214 clearUndoRedo = cbClearUndoRedo.isSelected(); 215 Main.pref.put("purge.clear_undo_redo", clearUndoRedo); 216 } 217 218 Main.main.undoRedo.add(new PurgeCommand(Main.getLayerManager().getEditLayer(), toPurgeChecked, makeIncomplete)); 219 220 if (clearUndoRedo) { 221 Main.main.undoRedo.clean(); 222 getLayerManager().getEditDataSet().clearSelectionHistory(); 223 } 224 } 225 226 private JPanel buildPanel(boolean modified) { 227 JPanel pnl = new JPanel(new GridBagLayout()); 228 229 pnl.add(Box.createRigidArea(new Dimension(400, 0)), GBC.eol().fill(GBC.HORIZONTAL)); 230 231 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 232 pnl.add(new JLabel("<html>"+ 233 tr("This operation makes JOSM forget the selected objects.<br> " + 234 "They will be removed from the layer, but <i>not</i> deleted<br> " + 235 "on the server when uploading.")+"</html>", 236 ImageProvider.get("purge"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL)); 237 238 if (!toPurgeAdditionally.isEmpty()) { 239 pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 240 pnl.add(new JLabel("<html>"+ 241 tr("The following dependent objects will be purged<br> " + 242 "in addition to the selected objects:")+"</html>", 243 ImageProvider.get("warning-small"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL)); 244 245 toPurgeAdditionally.sort((o1, o2) -> { 246 int type = o2.getType().compareTo(o1.getType()); 247 if (type != 0) 248 return type; 249 return Long.compare(o1.getUniqueId(), o2.getUniqueId()); 250 }); 251 JList<OsmPrimitive> list = new JList<>(toPurgeAdditionally.toArray(new OsmPrimitive[toPurgeAdditionally.size()])); 252 /* force selection to be active for all entries */ 253 list.setCellRenderer(new OsmPrimitivRenderer() { 254 @Override 255 public Component getListCellRendererComponent(JList<? extends OsmPrimitive> list, 256 OsmPrimitive value, 257 int index, 258 boolean isSelected, 259 boolean cellHasFocus) { 260 return super.getListCellRendererComponent(list, value, index, true, false); 261 } 262 }); 263 JScrollPane scroll = new JScrollPane(list); 264 scroll.setPreferredSize(new Dimension(250, 300)); 265 scroll.setMinimumSize(new Dimension(250, 300)); 266 pnl.add(scroll, GBC.std().fill(GBC.BOTH).weight(1.0, 1.0)); 267 268 JButton addToSelection = new JButton(new AbstractAction() { 269 { 270 putValue(SHORT_DESCRIPTION, tr("Add to selection")); 271 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 272 } 273 274 @Override 275 public void actionPerformed(ActionEvent e) { 276 layer.data.addSelected(toPurgeAdditionally); 277 } 278 }); 279 addToSelection.setMargin(new Insets(0, 0, 0, 0)); 280 pnl.add(addToSelection, GBC.eol().anchor(GBC.SOUTHWEST).weight(0.0, 1.0).insets(2, 0, 0, 3)); 281 } 282 283 if (modified) { 284 pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 285 pnl.add(new JLabel("<html>"+tr("Some of the objects are modified.<br> " + 286 "Proceed, if these changes should be discarded."+"</html>"), 287 ImageProvider.get("warning-small"), JLabel.LEFT), 288 GBC.eol().fill(GBC.HORIZONTAL)); 289 } 290 291 cbClearUndoRedo = new JCheckBox(tr("Clear Undo/Redo buffer")); 292 cbClearUndoRedo.setSelected(Main.pref.getBoolean("purge.clear_undo_redo", false)); 293 294 pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 295 pnl.add(cbClearUndoRedo, GBC.eol()); 296 return pnl; 297 } 298 299 @Override 300 protected void updateEnabledState() { 301 DataSet ds = getLayerManager().getEditDataSet(); 302 if (ds == null) { 303 setEnabled(false); 304 } else { 305 setEnabled(!ds.selectionEmpty()); 306 } 307 } 308 309 @Override 310 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 311 setEnabled(selection != null && !selection.isEmpty()); 312 } 313 314 private static boolean hasOnlyIncompleteMembers( 315 Relation r, Collection<OsmPrimitive> toPurge, Collection<? extends OsmPrimitive> moreToPurge) { 316 for (RelationMember m : r.getMembers()) { 317 if (!m.getMember().isIncomplete() && !toPurge.contains(m.getMember()) && !moreToPurge.contains(m.getMember())) 318 return false; 319 } 320 return true; 321 } 322}