// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.PurgeCommand;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
/**
* The action to purge the selected primitives, i.e. remove them from the
* data layer, or remove their content and make them incomplete.
*
* This means, the deleted flag is not affected and JOSM simply forgets
* about these primitives.
*
* This action is undo-able. In order not to break previous commands in the
* undo buffer, we must re-add the identical object (and not semantically equal ones).
*
* @since 3431
*/
public class PurgeAction extends JosmAction {
protected transient OsmDataLayer layer;
protected JCheckBox cbClearUndoRedo;
protected boolean modified;
protected transient Set<OsmPrimitive> toPurge;
/**
* finally, contains all objects that are purged
*/
protected transient Set<OsmPrimitive> toPurgeChecked;
/**
* Subset of toPurgeChecked. Marks primitives that remain in the dataset, but incomplete.
*/
protected transient Set<OsmPrimitive> makeIncomplete;
/**
* Subset of toPurgeChecked. Those that have not been in the selection.
*/
protected transient List<OsmPrimitive> toPurgeAdditionally;
/**
* Constructs a new {@code PurgeAction}.
*/
public PurgeAction() {
this(true);
}
/**
* Constructs a new {@code PurgeAction} with optional shortcut.
* @param addShortcut controls whether the shortcut should be registered or not
* @since 11611
*/
public PurgeAction(boolean addShortcut) {
/* translator note: other expressions for "purge" might be "forget", "clean", "obliterate", "prune" */
super(tr("Purge..."), "purge", tr("Forget objects but do not delete them on server when uploading."), addShortcut ?
Shortcut.registerShortcut("system:purge", tr("Edit: {0}", tr("Purge")), KeyEvent.VK_P, Shortcut.CTRL_SHIFT)
: null, true);
putValue("help", HelpUtil.ht("/Action/Purge"));
}
/** force selection to be active for all entries */
static class SelectionForcedOsmPrimitivRenderer extends OsmPrimitivRenderer {
@Override
public Component getListCellRendererComponent(JList<? extends OsmPrimitive> list,
OsmPrimitive value, int index, boolean isSelected, boolean cellHasFocus) {
return super.getListCellRendererComponent(list, value, index, true, false);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (!isEnabled())
return;
PurgeCommand cmd = getPurgeCommand(getLayerManager().getEditDataSet().getAllSelected());
boolean clearUndoRedo = false;
if (!GraphicsEnvironment.isHeadless()) {
final boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
"purge", Main.parent, buildPanel(modified), tr("Confirm Purging"),
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_OPTION);
if (!answer)
return;
clearUndoRedo = cbClearUndoRedo.isSelected();
Main.pref.put("purge.clear_undo_redo", clearUndoRedo);
}
Main.main.undoRedo.add(cmd);
if (clearUndoRedo) {
Main.main.undoRedo.clean();
getLayerManager().getEditDataSet().clearSelectionHistory();
}
}
/**
* Creates command to purge selected OSM primitives.
* @param sel selected OSM primitives
* @return command to purge selected OSM primitives
* @since 11252
*/
public PurgeCommand getPurgeCommand(Collection<OsmPrimitive> sel) {
layer = Main.getLayerManager().getEditLayer();
toPurge = new HashSet<>(sel);
toPurgeAdditionally = new ArrayList<>();
toPurgeChecked = new HashSet<>();
// Add referrer, unless the object to purge is not new and the parent is a relation
Set<OsmPrimitive> toPurgeRecursive = new HashSet<>();
while (!toPurge.isEmpty()) {
for (OsmPrimitive osm: toPurge) {
for (OsmPrimitive parent: osm.getReferrers()) {
if (toPurge.contains(parent) || toPurgeChecked.contains(parent) || toPurgeRecursive.contains(parent)) {
continue;
}
if (parent instanceof Way || (parent instanceof Relation && osm.isNew())) {
toPurgeAdditionally.add(parent);
toPurgeRecursive.add(parent);
}
}
toPurgeChecked.add(osm);
}
toPurge = toPurgeRecursive;
toPurgeRecursive = new HashSet<>();
}
makeIncomplete = new HashSet<>();
// Find the objects that will be incomplete after purging.
// At this point, all parents of new to-be-purged primitives are
// also to-be-purged and
// all parents of not-new to-be-purged primitives are either
// to-be-purged or of type relation.
TOP:
for (OsmPrimitive child : toPurgeChecked) {
if (child.isNew()) {
continue;
}
for (OsmPrimitive parent : child.getReferrers()) {
if (parent instanceof Relation && !toPurgeChecked.contains(parent)) {
makeIncomplete.add(child);
continue TOP;
}
}
}
// Add untagged way nodes. Do not add nodes that have other referrers not yet to-be-purged.
if (Main.pref.getBoolean("purge.add_untagged_waynodes", true)) {
Set<OsmPrimitive> wayNodes = new HashSet<>();
for (OsmPrimitive osm : toPurgeChecked) {
if (osm instanceof Way) {
Way w = (Way) osm;
NODE:
for (Node n : w.getNodes()) {
if (n.isTagged() || toPurgeChecked.contains(n)) {
continue;
}
for (OsmPrimitive ref : n.getReferrers()) {
if (ref != w && !toPurgeChecked.contains(ref)) {
continue NODE;
}
}
wayNodes.add(n);
}
}
}
toPurgeChecked.addAll(wayNodes);
toPurgeAdditionally.addAll(wayNodes);
}
if (Main.pref.getBoolean("purge.add_relations_with_only_incomplete_members", true)) {
Set<Relation> relSet = new HashSet<>();
for (OsmPrimitive osm : toPurgeChecked) {
for (OsmPrimitive parent : osm.getReferrers()) {
if (parent instanceof Relation
&& !(toPurgeChecked.contains(parent))
&& hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relSet)) {
relSet.add((Relation) parent);
}
}
}
// Add higher level relations (list gets extended while looping over it)
List<Relation> relLst = new ArrayList<>(relSet);
for (int i = 0; i < relLst.size(); ++i) { // foreach loop not applicable since list gets extended while looping over it
for (OsmPrimitive parent : relLst.get(i).getReferrers()) {
if (!(toPurgeChecked.contains(parent))
&& hasOnlyIncompleteMembers((Relation) parent, toPurgeChecked, relLst)) {
relLst.add((Relation) parent);
}
}
}
relSet = new HashSet<>(relLst);
toPurgeChecked.addAll(relSet);
toPurgeAdditionally.addAll(relSet);
}
modified = false;
for (OsmPrimitive osm : toPurgeChecked) {
if (osm.isModified()) {
modified = true;
break;
}
}
return layer != null ? new PurgeCommand(layer, toPurgeChecked, makeIncomplete) :
new PurgeCommand(toPurgeChecked.iterator().next().getDataSet(), toPurgeChecked, makeIncomplete);
}
private JPanel buildPanel(boolean modified) {
JPanel pnl = new JPanel(new GridBagLayout());
pnl.add(Box.createRigidArea(new Dimension(400, 0)), GBC.eol().fill(GBC.HORIZONTAL));
pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
pnl.add(new JLabel("<html>"+
tr("This operation makes JOSM forget the selected objects.<br> " +
"They will be removed from the layer, but <i>not</i> deleted<br> " +
"on the server when uploading.")+"</html>",
ImageProvider.get("purge"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
if (!toPurgeAdditionally.isEmpty()) {
pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
pnl.add(new JLabel("<html>"+
tr("The following dependent objects will be purged<br> " +
"in addition to the selected objects:")+"</html>",
ImageProvider.get("warning-small"), JLabel.LEFT), GBC.eol().fill(GBC.HORIZONTAL));
toPurgeAdditionally.sort((o1, o2) -> {
int type = o2.getType().compareTo(o1.getType());
if (type != 0)
return type;
return Long.compare(o1.getUniqueId(), o2.getUniqueId());
});
JList<OsmPrimitive> list = new JList<>(toPurgeAdditionally.toArray(new OsmPrimitive[toPurgeAdditionally.size()]));
/* force selection to be active for all entries */
list.setCellRenderer(new SelectionForcedOsmPrimitivRenderer());
JScrollPane scroll = new JScrollPane(list);
scroll.setPreferredSize(new Dimension(250, 300));
scroll.setMinimumSize(new Dimension(250, 300));
pnl.add(scroll, GBC.std().fill(GBC.BOTH).weight(1.0, 1.0));
JButton addToSelection = new JButton(new AbstractAction() {
{
putValue(SHORT_DESCRIPTION, tr("Add to selection"));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
}
@Override
public void actionPerformed(ActionEvent e) {
layer.data.addSelected(toPurgeAdditionally);
}
});
addToSelection.setMargin(new Insets(0, 0, 0, 0));
pnl.add(addToSelection, GBC.eol().anchor(GBC.SOUTHWEST).weight(0.0, 1.0).insets(2, 0, 0, 3));
}
if (modified) {
pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
pnl.add(new JLabel("<html>"+tr("Some of the objects are modified.<br> " +
"Proceed, if these changes should be discarded."+"</html>"),
ImageProvider.get("warning-small"), JLabel.LEFT),
GBC.eol().fill(GBC.HORIZONTAL));
}
cbClearUndoRedo = new JCheckBox(tr("Clear Undo/Redo buffer"));
cbClearUndoRedo.setSelected(Main.pref.getBoolean("purge.clear_undo_redo", false));
pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
pnl.add(cbClearUndoRedo, GBC.eol());
return pnl;
}
@Override
protected void updateEnabledState() {
DataSet ds = getLayerManager().getEditDataSet();
setEnabled(ds != null && !ds.selectionEmpty());
}
@Override
protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
setEnabled(selection != null && !selection.isEmpty());
}
private static boolean hasOnlyIncompleteMembers(
Relation r, Collection<OsmPrimitive> toPurge, Collection<? extends OsmPrimitive> moreToPurge) {
for (RelationMember m : r.getMembers()) {
if (!m.getMember().isIncomplete() && !toPurge.contains(m.getMember()) && !moreToPurge.contains(m.getMember()))
return false;
}
return true;
}
}