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