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