001//License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import java.awt.GridBagLayout;
005import java.awt.geom.Area;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.LinkedHashMap;
010import java.util.Map;
011import java.util.Map.Entry;
012
013import javax.swing.JLabel;
014import javax.swing.JOptionPane;
015import javax.swing.JPanel;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.coor.EastNorth;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.osm.Node;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.PrimitiveData;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.Way;
025import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
026import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
027import org.openstreetmap.josm.gui.layer.Layer;
028import org.openstreetmap.josm.gui.layer.OsmDataLayer;
029import org.openstreetmap.josm.tools.CheckParameterUtil;
030
031/**
032 * Classes implementing Command modify a dataset in a specific way. A command is
033 * one atomic action on a specific dataset, such as move or delete.
034 *
035 * The command remembers the {@link OsmDataLayer} it is operating on.
036 *
037 * @author imi
038 */
039abstract public class Command extends PseudoCommand {
040
041    private static final class CloneVisitor extends AbstractVisitor {
042        public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<OsmPrimitive, PrimitiveData>();
043
044        @Override
045        public void visit(Node n) {
046            orig.put(n, n.save());
047        }
048        @Override
049        public void visit(Way w) {
050            orig.put(w, w.save());
051        }
052        @Override
053        public void visit(Relation e) {
054            orig.put(e, e.save());
055        }
056    }
057
058    /**
059     * Small helper for holding the interesting part of the old data state of the objects.
060     */
061    public static class OldNodeState {
062
063        final LatLon latlon;
064        final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
065        final boolean modified;
066
067        /**
068         * Constructs a new {@code OldNodeState} for the given node.
069         * @param node The node whose state has to be remembered
070         */
071        public OldNodeState(Node node){
072            latlon = node.getCoor();
073            eastNorth = node.getEastNorth();
074            modified = node.isModified();
075        }
076    }
077
078    /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
079    private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<OsmPrimitive, PrimitiveData>();
080
081    /** the layer which this command is applied to */
082    private final OsmDataLayer layer;
083
084    /**
085     * Creates a new command in the context of the current edit layer, if any
086     */
087    public Command() {
088        this.layer = Main.main.getEditLayer();
089    }
090
091    /**
092     * Creates a new command in the context of a specific data layer
093     *
094     * @param layer the data layer. Must not be null.
095     * @throws IllegalArgumentException thrown if layer is null
096     */
097    public Command(OsmDataLayer layer) throws IllegalArgumentException {
098        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
099        this.layer = layer;
100    }
101
102    /**
103     * Executes the command on the dataset. This implementation will remember all
104     * primitives returned by fillModifiedData for restoring them on undo.
105     * @return true
106     */
107    public boolean executeCommand() {
108        CloneVisitor visitor = new CloneVisitor();
109        Collection<OsmPrimitive> all = new ArrayList<OsmPrimitive>();
110        fillModifiedData(all, all, all);
111        for (OsmPrimitive osm : all) {
112            osm.accept(visitor);
113        }
114        cloneMap = visitor.orig;
115        return true;
116    }
117
118    /**
119     * Undoes the command.
120     * It can be assumed that all objects are in the same state they were before.
121     * It can also be assumed that executeCommand was called exactly once before.
122     *
123     * This implementation undoes all objects stored by a former call to executeCommand.
124     */
125    public void undoCommand() {
126        for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
127            OsmPrimitive primitive = e.getKey();
128            if (primitive.getDataSet() != null) {
129                e.getKey().load(e.getValue());
130            }
131        }
132    }
133
134    /**
135     * Called when a layer has been removed to have the command remove itself from
136     * any buffer if it is not longer applicable to the dataset (e.g. it was part of
137     * the removed layer)
138     *
139     * @param oldLayer the old layer
140     * @return true if this command
141     */
142    public boolean invalidBecauselayerRemoved(Layer oldLayer) {
143        if (!(oldLayer instanceof OsmDataLayer))
144            return false;
145        return layer == oldLayer;
146    }
147
148    /**
149     * Lets other commands access the original version
150     * of the object. Usually for undoing.
151     * @param osm The requested OSM object
152     * @return The original version of the requested object, if any
153     */
154    public PrimitiveData getOrig(OsmPrimitive osm) {
155        return cloneMap.get(osm);
156    }
157
158    /**
159     * Replies the layer this command is (or was) applied to.
160     *
161     */
162    protected  OsmDataLayer getLayer() {
163        return layer;
164    }
165
166    /**
167     * Fill in the changed data this command operates on.
168     * Add to the lists, don't clear them.
169     *
170     * @param modified The modified primitives
171     * @param deleted The deleted primitives
172     * @param added The added primitives
173     */
174    abstract public void fillModifiedData(Collection<OsmPrimitive> modified,
175            Collection<OsmPrimitive> deleted,
176            Collection<OsmPrimitive> added);
177
178    /**
179     * Return the primitives that take part in this command.
180     */
181    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
182        return cloneMap.keySet();
183    }
184
185    /**
186     * Check whether user is about to operate on data outside of the download area.
187     * Request confirmation if he is.
188     *
189     * @param operation the operation name which is used for setting some preferences
190     * @param dialogTitle the title of the dialog being displayed
191     * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
192     * @param incompleteDialogMessage the message text to be displayed when data is incomplete
193     * @param area the area used to determine whether data is outlying
194     * @param primitives the primitives to operate on
195     * @param ignore {@code null} or a primitive to be ignored
196     * @return true, if operating on outlying primitives is OK; false, otherwise
197     */
198    public static boolean checkAndConfirmOutlyingOperation(String operation,
199            String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
200            Area area, Collection<? extends OsmPrimitive> primitives,
201            Collection<? extends OsmPrimitive> ignore) {
202        boolean outside = false;
203        boolean incomplete = false;
204        for (OsmPrimitive osm : primitives) {
205            if (osm.isIncomplete()) {
206                incomplete = true;
207            } else if (area != null && isOutlying(osm, area)
208                    && (ignore == null || !ignore.contains(osm))) {
209                outside = true;
210            }
211        }
212        if (outside) {
213            JPanel msg = new JPanel(new GridBagLayout());
214            msg.add(new JLabel("<html>" + outsideDialogMessage + "</html>"));
215            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
216                    operation + "_outside_nodes",
217                    Main.parent,
218                    msg,
219                    dialogTitle,
220                    JOptionPane.YES_NO_OPTION,
221                    JOptionPane.QUESTION_MESSAGE,
222                    JOptionPane.YES_OPTION);
223            if(!answer)
224                return false;
225        }
226        if (incomplete) {
227            JPanel msg = new JPanel(new GridBagLayout());
228            msg.add(new JLabel("<html>" + incompleteDialogMessage + "</html>"));
229            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
230                    operation + "_incomplete",
231                    Main.parent,
232                    msg,
233                    dialogTitle,
234                    JOptionPane.YES_NO_OPTION,
235                    JOptionPane.QUESTION_MESSAGE,
236                    JOptionPane.YES_OPTION);
237            if(!answer)
238                return false;
239        }
240        return true;
241    }
242
243    private static boolean isOutlying(OsmPrimitive osm, Area area) {
244        if (osm instanceof Node && !osm.isNewOrUndeleted()) {
245            return !((Node) osm).getCoor().isIn(area);
246        } else if (osm instanceof Way) {
247            for (Node n : ((Way) osm).getNodes()) {
248                if (isOutlying(n, area)) {
249                    return true;
250                }
251            }
252            return false;
253        }
254        return false;
255    }
256}