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