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