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