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