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