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