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 /** 140 * Creates a new command in the context of the current edit layer, if any 141 */ 142 public Command() { 143 this.layer = Main.getLayerManager().getEditLayer(); 144 } 145 146 /** 147 * Creates a new command in the context of a specific data layer 148 * 149 * @param layer the data layer. Must not be null. 150 * @throws IllegalArgumentException if layer is null 151 */ 152 public Command(OsmDataLayer layer) { 153 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 154 this.layer = layer; 155 } 156 157 /** 158 * Executes the command on the dataset. This implementation will remember all 159 * primitives returned by fillModifiedData for restoring them on undo. 160 * <p> 161 * The layer should be invalidated after execution so that it can be re-painted. 162 * @return true 163 * @see #invalidateAffectedLayers() 164 */ 165 public boolean executeCommand() { 166 CloneVisitor visitor = new CloneVisitor(); 167 Collection<OsmPrimitive> all = new ArrayList<>(); 168 fillModifiedData(all, all, all); 169 for (OsmPrimitive osm : all) { 170 osm.accept(visitor); 171 } 172 cloneMap = visitor.orig; 173 return true; 174 } 175 176 /** 177 * Undoes the command. 178 * It can be assumed that all objects are in the same state they were before. 179 * It can also be assumed that executeCommand was called exactly once before. 180 * 181 * This implementation undoes all objects stored by a former call to executeCommand. 182 */ 183 public void undoCommand() { 184 for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) { 185 OsmPrimitive primitive = e.getKey(); 186 if (primitive.getDataSet() != null) { 187 e.getKey().load(e.getValue()); 188 } 189 } 190 } 191 192 /** 193 * Called when a layer has been removed to have the command remove itself from 194 * any buffer if it is not longer applicable to the dataset (e.g. it was part of 195 * the removed layer) 196 * 197 * @param oldLayer the old layer that was removed 198 * @return true if this command is invalid after that layer is removed. 199 */ 200 public boolean invalidBecauselayerRemoved(Layer oldLayer) { 201 return layer == oldLayer; 202 } 203 204 /** 205 * Lets other commands access the original version 206 * of the object. Usually for undoing. 207 * @param osm The requested OSM object 208 * @return The original version of the requested object, if any 209 */ 210 public PrimitiveData getOrig(OsmPrimitive osm) { 211 return cloneMap.get(osm); 212 } 213 214 /** 215 * Replies the layer this command is (or was) applied to. 216 * @return the layer this command is (or was) applied to 217 */ 218 protected OsmDataLayer getLayer() { 219 return layer; 220 } 221 222 /** 223 * Gets the data set this command affects. 224 * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found. 225 * @since 10467 226 */ 227 public DataSet getAffectedDataSet() { 228 return layer == null ? null : layer.data; 229 } 230 231 /** 232 * Fill in the changed data this command operates on. 233 * Add to the lists, don't clear them. 234 * 235 * @param modified The modified primitives 236 * @param deleted The deleted primitives 237 * @param added The added primitives 238 */ 239 public abstract void fillModifiedData(Collection<OsmPrimitive> modified, 240 Collection<OsmPrimitive> deleted, 241 Collection<OsmPrimitive> added); 242 243 /** 244 * Return the primitives that take part in this command. 245 * The collection is computed during execution. 246 */ 247 @Override 248 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 249 return cloneMap.keySet(); 250 } 251 252 /** 253 * Check whether user is about to operate on data outside of the download area. 254 * 255 * @param primitives the primitives to operate on 256 * @param ignore {@code null} or a primitive to be ignored 257 * @return true, if operating on outlying primitives is OK; false, otherwise 258 */ 259 public static int checkOutlyingOrIncompleteOperation( 260 Collection<? extends OsmPrimitive> primitives, 261 Collection<? extends OsmPrimitive> ignore) { 262 int res = 0; 263 for (OsmPrimitive osm : primitives) { 264 if (osm.isIncomplete()) { 265 res |= IS_INCOMPLETE; 266 } else if (osm.isOutsideDownloadArea() 267 && (ignore == null || !ignore.contains(osm))) { 268 res |= IS_OUTSIDE; 269 } 270 } 271 return res; 272 } 273 274 /** 275 * Check whether user is about to operate on data outside of the download area. 276 * Request confirmation if he is. 277 * 278 * @param operation the operation name which is used for setting some preferences 279 * @param dialogTitle the title of the dialog being displayed 280 * @param outsideDialogMessage the message text to be displayed when data is outside of the download area 281 * @param incompleteDialogMessage the message text to be displayed when data is incomplete 282 * @param primitives the primitives to operate on 283 * @param ignore {@code null} or a primitive to be ignored 284 * @return true, if operating on outlying primitives is OK; false, otherwise 285 */ 286 public static boolean checkAndConfirmOutlyingOperation(String operation, 287 String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, 288 Collection<? extends OsmPrimitive> primitives, 289 Collection<? extends OsmPrimitive> ignore) { 290 int checkRes = checkOutlyingOrIncompleteOperation(primitives, ignore); 291 if ((checkRes & IS_OUTSIDE) != 0) { 292 JPanel msg = new JPanel(new GridBagLayout()); 293 msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>")); 294 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 295 operation + "_outside_nodes", 296 Main.parent, 297 msg, 298 dialogTitle, 299 JOptionPane.YES_NO_OPTION, 300 JOptionPane.QUESTION_MESSAGE, 301 JOptionPane.YES_OPTION); 302 if (!answer) 303 return false; 304 } 305 if ((checkRes & IS_INCOMPLETE) != 0) { 306 JPanel msg = new JPanel(new GridBagLayout()); 307 msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>")); 308 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 309 operation + "_incomplete", 310 Main.parent, 311 msg, 312 dialogTitle, 313 JOptionPane.YES_NO_OPTION, 314 JOptionPane.QUESTION_MESSAGE, 315 JOptionPane.YES_OPTION); 316 if (!answer) 317 return false; 318 } 319 return true; 320 } 321 322 @Override 323 public int hashCode() { 324 return Objects.hash(cloneMap, layer); 325 } 326 327 @Override 328 public boolean equals(Object obj) { 329 if (this == obj) return true; 330 if (obj == null || getClass() != obj.getClass()) return false; 331 Command command = (Command) obj; 332 return Objects.equals(cloneMap, command.cloneMap) && 333 Objects.equals(layer, command.layer); 334 } 335 336 /** 337 * Invalidate all layers that were affected by this command. 338 * @see Layer#invalidate() 339 */ 340 public void invalidateAffectedLayers() { 341 OsmDataLayer layer = getLayer(); 342 if (layer != null) { 343 layer.invalidate(); 344 } 345 } 346}