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