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