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