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}