// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.command; import java.awt.GridBagLayout; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; 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.PrimitiveData; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.widgets.JMultilineLabel; import org.openstreetmap.josm.tools.CheckParameterUtil; /** * Classes implementing Command modify a dataset in a specific way. A command is * one atomic action on a specific dataset, such as move or delete. * * The command remembers the {@link OsmDataLayer} it is operating on. * * @author imi * @since 21 (creation) * @since 10599 (signature) */ public abstract class Command implements PseudoCommand { /** IS_OK : operation is okay */ public static final int IS_OK = 0; /** IS_OUTSIDE : operation on element outside of download area */ public static final int IS_OUTSIDE = 1; /** IS_INCOMPLETE: operation on incomplete target */ public static final int IS_INCOMPLETE = 2; private static final class CloneVisitor extends AbstractVisitor { public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>(); @Override public void visit(Node n) { orig.put(n, n.save()); } @Override public void visit(Way w) { orig.put(w, w.save()); } @Override public void visit(Relation e) { orig.put(e, e.save()); } } /** * Small helper for holding the interesting part of the old data state of the objects. */ public static class OldNodeState { private final LatLon latLon; private final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement private final boolean modified; /** * Constructs a new {@code OldNodeState} for the given node. * @param node The node whose state has to be remembered */ public OldNodeState(Node node) { latLon = node.getCoor(); eastNorth = node.getEastNorth(); modified = node.isModified(); } /** * Returns old lat/lon. * @return old lat/lon * @see Node#getCoor() * @since 10248 */ public final LatLon getLatLon() { return latLon; } /** * Returns old east/north. * @return old east/north * @see Node#getEastNorth() */ public final EastNorth getEastNorth() { return eastNorth; } /** * Returns old modified state. * @return old modified state * @see Node #isModified() */ public final boolean isModified() { return modified; } @Override public int hashCode() { return Objects.hash(latLon, eastNorth, modified); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; OldNodeState that = (OldNodeState) obj; return modified == that.modified && Objects.equals(latLon, that.latLon) && Objects.equals(eastNorth, that.eastNorth); } } /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */ private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>(); /** the layer which this command is applied to */ private final OsmDataLayer layer; /** the dataset which this command is applied to */ private final DataSet data; /** * Creates a new command in the context of the current edit layer, if any */ public Command() { this.layer = Main.getLayerManager().getEditLayer(); this.data = layer != null ? layer.data : null; } /** * Creates a new command in the context of a specific data layer * * @param layer the data layer. Must not be null. * @throws IllegalArgumentException if layer is null */ public Command(OsmDataLayer layer) { CheckParameterUtil.ensureParameterNotNull(layer, "layer"); this.layer = layer; this.data = layer.data; } /** * Creates a new command in the context of a specific data set, without data layer * * @param data the data set. Must not be null. * @throws IllegalArgumentException if data is null * @since 11240 */ public Command(DataSet data) { CheckParameterUtil.ensureParameterNotNull(data, "data"); this.layer = null; this.data = data; } /** * Executes the command on the dataset. This implementation will remember all * primitives returned by fillModifiedData for restoring them on undo. * <p> * The layer should be invalidated after execution so that it can be re-painted. * @return true * @see #invalidateAffectedLayers() */ public boolean executeCommand() { CloneVisitor visitor = new CloneVisitor(); Collection<OsmPrimitive> all = new ArrayList<>(); fillModifiedData(all, all, all); for (OsmPrimitive osm : all) { osm.accept(visitor); } cloneMap = visitor.orig; return true; } /** * Undoes the command. * It can be assumed that all objects are in the same state they were before. * It can also be assumed that executeCommand was called exactly once before. * * This implementation undoes all objects stored by a former call to executeCommand. */ public void undoCommand() { for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) { OsmPrimitive primitive = e.getKey(); if (primitive.getDataSet() != null) { e.getKey().load(e.getValue()); } } } /** * Called when a layer has been removed to have the command remove itself from * any buffer if it is not longer applicable to the dataset (e.g. it was part of * the removed layer) * * @param oldLayer the old layer that was removed * @return true if this command is invalid after that layer is removed. */ public boolean invalidBecauselayerRemoved(Layer oldLayer) { return layer == oldLayer; } /** * Lets other commands access the original version * of the object. Usually for undoing. * @param osm The requested OSM object * @return The original version of the requested object, if any */ public PrimitiveData getOrig(OsmPrimitive osm) { return cloneMap.get(osm); } /** * Replies the layer this command is (or was) applied to. * @return the layer this command is (or was) applied to */ protected OsmDataLayer getLayer() { return layer; } /** * Gets the data set this command affects. * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found. * @since 10467 */ public DataSet getAffectedDataSet() { return data; } /** * Fill in the changed data this command operates on. * Add to the lists, don't clear them. * * @param modified The modified primitives * @param deleted The deleted primitives * @param added The added primitives */ public abstract void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added); /** * Return the primitives that take part in this command. * The collection is computed during execution. */ @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { return cloneMap.keySet(); } /** * Check whether user is about to operate on data outside of the download area. * * @param primitives the primitives to operate on * @param ignore {@code null} or a primitive to be ignored * @return true, if operating on outlying primitives is OK; false, otherwise */ public static int checkOutlyingOrIncompleteOperation( Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) { int res = 0; for (OsmPrimitive osm : primitives) { if (osm.isIncomplete()) { res |= IS_INCOMPLETE; } else if (osm.isOutsideDownloadArea() && (ignore == null || !ignore.contains(osm))) { res |= IS_OUTSIDE; } } return res; } /** * Check whether user is about to operate on data outside of the download area. * Request confirmation if he is. * * @param operation the operation name which is used for setting some preferences * @param dialogTitle the title of the dialog being displayed * @param outsideDialogMessage the message text to be displayed when data is outside of the download area * @param incompleteDialogMessage the message text to be displayed when data is incomplete * @param primitives the primitives to operate on * @param ignore {@code null} or a primitive to be ignored * @return true, if operating on outlying primitives is OK; false, otherwise */ public static boolean checkAndConfirmOutlyingOperation(String operation, String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) { int checkRes = checkOutlyingOrIncompleteOperation(primitives, ignore); if ((checkRes & IS_OUTSIDE) != 0) { JPanel msg = new JPanel(new GridBagLayout()); msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>")); boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( operation + "_outside_nodes", Main.parent, msg, dialogTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION); if (!answer) return false; } if ((checkRes & IS_INCOMPLETE) != 0) { JPanel msg = new JPanel(new GridBagLayout()); msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>")); boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( operation + "_incomplete", Main.parent, msg, dialogTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION); if (!answer) return false; } return true; } @Override public int hashCode() { return Objects.hash(cloneMap, layer, data); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Command command = (Command) obj; return Objects.equals(cloneMap, command.cloneMap) && Objects.equals(layer, command.layer) && Objects.equals(data, command.data); } /** * Invalidate all layers that were affected by this command. * @see Layer#invalidate() */ public void invalidateAffectedLayers() { OsmDataLayer layer = getLayer(); if (layer != null) { layer.invalidate(); } } }