[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[21] | 2 | package org.openstreetmap.josm.command;
|
---|
| 3 |
|
---|
[4458] | 4 | import java.awt.GridBagLayout;
|
---|
[3034] | 5 | import java.util.ArrayList;
|
---|
[22] | 6 | import java.util.Collection;
|
---|
[146] | 7 | import java.util.HashMap;
|
---|
[3034] | 8 | import java.util.LinkedHashMap;
|
---|
[146] | 9 | import java.util.Map;
|
---|
[86] | 10 | import java.util.Map.Entry;
|
---|
[9371] | 11 | import java.util.Objects;
|
---|
[21] | 12 |
|
---|
[4458] | 13 | import javax.swing.JOptionPane;
|
---|
| 14 | import javax.swing.JPanel;
|
---|
[94] | 15 |
|
---|
[630] | 16 | import org.openstreetmap.josm.Main;
|
---|
[6173] | 17 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
| 18 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[10467] | 19 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[146] | 20 | import org.openstreetmap.josm.data.osm.Node;
|
---|
[22] | 21 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
[2284] | 22 | import org.openstreetmap.josm.data.osm.PrimitiveData;
|
---|
[1523] | 23 | import org.openstreetmap.josm.data.osm.Relation;
|
---|
[146] | 24 | import org.openstreetmap.josm.data.osm.Way;
|
---|
[1523] | 25 | import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
|
---|
[4458] | 26 | import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
|
---|
[12636] | 27 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
[304] | 28 | import org.openstreetmap.josm.gui.layer.Layer;
|
---|
| 29 | import org.openstreetmap.josm.gui.layer.OsmDataLayer;
|
---|
[6901] | 30 | import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
|
---|
[2844] | 31 | import org.openstreetmap.josm.tools.CheckParameterUtil;
|
---|
[21] | 32 |
|
---|
| 33 | /**
|
---|
| 34 | * Classes implementing Command modify a dataset in a specific way. A command is
|
---|
[22] | 35 | * one atomic action on a specific dataset, such as move or delete.
|
---|
[21] | 36 | *
|
---|
[5266] | 37 | * The command remembers the {@link OsmDataLayer} it is operating on.
|
---|
[2284] | 38 | *
|
---|
[21] | 39 | * @author imi
|
---|
[10599] | 40 | * @since 21 (creation)
|
---|
| 41 | * @since 10599 (signature)
|
---|
[21] | 42 | */
|
---|
[10599] | 43 | public abstract class Command implements PseudoCommand {
|
---|
[21] | 44 |
|
---|
[10948] | 45 | /** IS_OK : operation is okay */
|
---|
| 46 | public static final int IS_OK = 0;
|
---|
| 47 | /** IS_OUTSIDE : operation on element outside of download area */
|
---|
| 48 | public static final int IS_OUTSIDE = 1;
|
---|
| 49 | /** IS_INCOMPLETE: operation on incomplete target */
|
---|
| 50 | public static final int IS_INCOMPLETE = 2;
|
---|
| 51 |
|
---|
[1750] | 52 | private static final class CloneVisitor extends AbstractVisitor {
|
---|
[7005] | 53 | public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
|
---|
[304] | 54 |
|
---|
[6084] | 55 | @Override
|
---|
[1750] | 56 | public void visit(Node n) {
|
---|
[2284] | 57 | orig.put(n, n.save());
|
---|
[1750] | 58 | }
|
---|
[8510] | 59 |
|
---|
[6084] | 60 | @Override
|
---|
[1750] | 61 | public void visit(Way w) {
|
---|
[2284] | 62 | orig.put(w, w.save());
|
---|
[1750] | 63 | }
|
---|
[8510] | 64 |
|
---|
[6084] | 65 | @Override
|
---|
[1750] | 66 | public void visit(Relation e) {
|
---|
[2284] | 67 | orig.put(e, e.save());
|
---|
[1750] | 68 | }
|
---|
| 69 | }
|
---|
[304] | 70 |
|
---|
[6173] | 71 | /**
|
---|
| 72 | * Small helper for holding the interesting part of the old data state of the objects.
|
---|
| 73 | */
|
---|
| 74 | public static class OldNodeState {
|
---|
| 75 |
|
---|
[10248] | 76 | private final LatLon latLon;
|
---|
[8285] | 77 | private final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
|
---|
| 78 | private final boolean modified;
|
---|
[6173] | 79 |
|
---|
| 80 | /**
|
---|
| 81 | * Constructs a new {@code OldNodeState} for the given node.
|
---|
| 82 | * @param node The node whose state has to be remembered
|
---|
| 83 | */
|
---|
[8510] | 84 | public OldNodeState(Node node) {
|
---|
[10248] | 85 | latLon = node.getCoor();
|
---|
[6173] | 86 | eastNorth = node.getEastNorth();
|
---|
| 87 | modified = node.isModified();
|
---|
| 88 | }
|
---|
[8285] | 89 |
|
---|
| 90 | /**
|
---|
| 91 | * Returns old lat/lon.
|
---|
| 92 | * @return old lat/lon
|
---|
| 93 | * @see Node#getCoor()
|
---|
[10248] | 94 | * @since 10248
|
---|
[8285] | 95 | */
|
---|
[10248] | 96 | public final LatLon getLatLon() {
|
---|
| 97 | return latLon;
|
---|
[8285] | 98 | }
|
---|
| 99 |
|
---|
| 100 | /**
|
---|
| 101 | * Returns old east/north.
|
---|
| 102 | * @return old east/north
|
---|
| 103 | * @see Node#getEastNorth()
|
---|
| 104 | */
|
---|
| 105 | public final EastNorth getEastNorth() {
|
---|
| 106 | return eastNorth;
|
---|
| 107 | }
|
---|
| 108 |
|
---|
| 109 | /**
|
---|
| 110 | * Returns old modified state.
|
---|
| 111 | * @return old modified state
|
---|
| 112 | * @see Node #isModified()
|
---|
| 113 | */
|
---|
| 114 | public final boolean isModified() {
|
---|
| 115 | return modified;
|
---|
| 116 | }
|
---|
[8447] | 117 |
|
---|
| 118 | @Override
|
---|
| 119 | public int hashCode() {
|
---|
[10248] | 120 | return Objects.hash(latLon, eastNorth, modified);
|
---|
[8447] | 121 | }
|
---|
| 122 |
|
---|
| 123 | @Override
|
---|
| 124 | public boolean equals(Object obj) {
|
---|
[9371] | 125 | if (this == obj) return true;
|
---|
| 126 | if (obj == null || getClass() != obj.getClass()) return false;
|
---|
| 127 | OldNodeState that = (OldNodeState) obj;
|
---|
| 128 | return modified == that.modified &&
|
---|
[10248] | 129 | Objects.equals(latLon, that.latLon) &&
|
---|
[9371] | 130 | Objects.equals(eastNorth, that.eastNorth);
|
---|
[8447] | 131 | }
|
---|
[6173] | 132 | }
|
---|
| 133 |
|
---|
[1750] | 134 | /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
|
---|
[7005] | 135 | private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>();
|
---|
[304] | 136 |
|
---|
[1750] | 137 | /** the layer which this command is applied to */
|
---|
[5759] | 138 | private final OsmDataLayer layer;
|
---|
[22] | 139 |
|
---|
[11240] | 140 | /** the dataset which this command is applied to */
|
---|
| 141 | private final DataSet data;
|
---|
| 142 |
|
---|
[5759] | 143 | /**
|
---|
| 144 | * Creates a new command in the context of the current edit layer, if any
|
---|
| 145 | */
|
---|
[1750] | 146 | public Command() {
|
---|
[12636] | 147 | this.layer = MainApplication.getLayerManager().getEditLayer();
|
---|
[11240] | 148 | this.data = layer != null ? layer.data : null;
|
---|
[1750] | 149 | }
|
---|
[1856] | 150 |
|
---|
[1750] | 151 | /**
|
---|
[1856] | 152 | * Creates a new command in the context of a specific data layer
|
---|
[2284] | 153 | *
|
---|
[2308] | 154 | * @param layer the data layer. Must not be null.
|
---|
[8291] | 155 | * @throws IllegalArgumentException if layer is null
|
---|
[1856] | 156 | */
|
---|
[8291] | 157 | public Command(OsmDataLayer layer) {
|
---|
[2844] | 158 | CheckParameterUtil.ensureParameterNotNull(layer, "layer");
|
---|
[1856] | 159 | this.layer = layer;
|
---|
[11240] | 160 | this.data = layer.data;
|
---|
[1856] | 161 | }
|
---|
| 162 |
|
---|
| 163 | /**
|
---|
[11240] | 164 | * Creates a new command in the context of a specific data set, without data layer
|
---|
| 165 | *
|
---|
| 166 | * @param data the data set. Must not be null.
|
---|
| 167 | * @throws IllegalArgumentException if data is null
|
---|
| 168 | * @since 11240
|
---|
| 169 | */
|
---|
| 170 | public Command(DataSet data) {
|
---|
| 171 | CheckParameterUtil.ensureParameterNotNull(data, "data");
|
---|
| 172 | this.layer = null;
|
---|
| 173 | this.data = data;
|
---|
| 174 | }
|
---|
| 175 |
|
---|
| 176 | /**
|
---|
[1750] | 177 | * Executes the command on the dataset. This implementation will remember all
|
---|
| 178 | * primitives returned by fillModifiedData for restoring them on undo.
|
---|
[10452] | 179 | * <p>
|
---|
| 180 | * The layer should be invalidated after execution so that it can be re-painted.
|
---|
[5759] | 181 | * @return true
|
---|
[10452] | 182 | * @see #invalidateAffectedLayers()
|
---|
[1750] | 183 | */
|
---|
| 184 | public boolean executeCommand() {
|
---|
| 185 | CloneVisitor visitor = new CloneVisitor();
|
---|
[7005] | 186 | Collection<OsmPrimitive> all = new ArrayList<>();
|
---|
[1750] | 187 | fillModifiedData(all, all, all);
|
---|
| 188 | for (OsmPrimitive osm : all) {
|
---|
[6009] | 189 | osm.accept(visitor);
|
---|
[1750] | 190 | }
|
---|
| 191 | cloneMap = visitor.orig;
|
---|
| 192 | return true;
|
---|
| 193 | }
|
---|
[86] | 194 |
|
---|
[1750] | 195 | /**
|
---|
| 196 | * Undoes the command.
|
---|
| 197 | * It can be assumed that all objects are in the same state they were before.
|
---|
| 198 | * It can also be assumed that executeCommand was called exactly once before.
|
---|
| 199 | *
|
---|
| 200 | * This implementation undoes all objects stored by a former call to executeCommand.
|
---|
| 201 | */
|
---|
| 202 | public void undoCommand() {
|
---|
[2284] | 203 | for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
|
---|
[2683] | 204 | OsmPrimitive primitive = e.getKey();
|
---|
| 205 | if (primitive.getDataSet() != null) {
|
---|
| 206 | e.getKey().load(e.getValue());
|
---|
| 207 | }
|
---|
[1750] | 208 | }
|
---|
| 209 | }
|
---|
[304] | 210 |
|
---|
[1750] | 211 | /**
|
---|
| 212 | * Called when a layer has been removed to have the command remove itself from
|
---|
| 213 | * any buffer if it is not longer applicable to the dataset (e.g. it was part of
|
---|
| 214 | * the removed layer)
|
---|
[2284] | 215 | *
|
---|
[10663] | 216 | * @param oldLayer the old layer that was removed
|
---|
| 217 | * @return true if this command is invalid after that layer is removed.
|
---|
[1750] | 218 | */
|
---|
| 219 | public boolean invalidBecauselayerRemoved(Layer oldLayer) {
|
---|
| 220 | return layer == oldLayer;
|
---|
| 221 | }
|
---|
[304] | 222 |
|
---|
[630] | 223 | /**
|
---|
| 224 | * Lets other commands access the original version
|
---|
| 225 | * of the object. Usually for undoing.
|
---|
[5759] | 226 | * @param osm The requested OSM object
|
---|
| 227 | * @return The original version of the requested object, if any
|
---|
[630] | 228 | */
|
---|
[2284] | 229 | public PrimitiveData getOrig(OsmPrimitive osm) {
|
---|
[5759] | 230 | return cloneMap.get(osm);
|
---|
[630] | 231 | }
|
---|
[94] | 232 |
|
---|
[1750] | 233 | /**
|
---|
| 234 | * Replies the layer this command is (or was) applied to.
|
---|
[8931] | 235 | * @return the layer this command is (or was) applied to
|
---|
[1750] | 236 | */
|
---|
[6881] | 237 | protected OsmDataLayer getLayer() {
|
---|
[1750] | 238 | return layer;
|
---|
| 239 | }
|
---|
[630] | 240 |
|
---|
[1750] | 241 | /**
|
---|
[10467] | 242 | * Gets the data set this command affects.
|
---|
| 243 | * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found.
|
---|
| 244 | * @since 10467
|
---|
| 245 | */
|
---|
| 246 | public DataSet getAffectedDataSet() {
|
---|
[11240] | 247 | return data;
|
---|
[10467] | 248 | }
|
---|
| 249 |
|
---|
| 250 | /**
|
---|
[1750] | 251 | * Fill in the changed data this command operates on.
|
---|
| 252 | * Add to the lists, don't clear them.
|
---|
| 253 | *
|
---|
| 254 | * @param modified The modified primitives
|
---|
| 255 | * @param deleted The deleted primitives
|
---|
| 256 | * @param added The added primitives
|
---|
| 257 | */
|
---|
[6883] | 258 | public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
|
---|
[1750] | 259 | Collection<OsmPrimitive> deleted,
|
---|
| 260 | Collection<OsmPrimitive> added);
|
---|
| 261 |
|
---|
[3262] | 262 | /**
|
---|
| 263 | * Return the primitives that take part in this command.
|
---|
[8945] | 264 | * The collection is computed during execution.
|
---|
[3262] | 265 | */
|
---|
[8931] | 266 | @Override
|
---|
| 267 | public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
|
---|
[3262] | 268 | return cloneMap.keySet();
|
---|
| 269 | }
|
---|
[1750] | 270 |
|
---|
[3262] | 271 | /**
|
---|
[4458] | 272 | * Check whether user is about to operate on data outside of the download area.
|
---|
[10948] | 273 | *
|
---|
| 274 | * @param primitives the primitives to operate on
|
---|
| 275 | * @param ignore {@code null} or a primitive to be ignored
|
---|
| 276 | * @return true, if operating on outlying primitives is OK; false, otherwise
|
---|
| 277 | */
|
---|
[10970] | 278 | public static int checkOutlyingOrIncompleteOperation(
|
---|
[10948] | 279 | Collection<? extends OsmPrimitive> primitives,
|
---|
| 280 | Collection<? extends OsmPrimitive> ignore) {
|
---|
| 281 | int res = 0;
|
---|
| 282 | for (OsmPrimitive osm : primitives) {
|
---|
| 283 | if (osm.isIncomplete()) {
|
---|
| 284 | res |= IS_INCOMPLETE;
|
---|
| 285 | } else if (osm.isOutsideDownloadArea()
|
---|
| 286 | && (ignore == null || !ignore.contains(osm))) {
|
---|
| 287 | res |= IS_OUTSIDE;
|
---|
| 288 | }
|
---|
| 289 | }
|
---|
| 290 | return res;
|
---|
| 291 | }
|
---|
| 292 |
|
---|
| 293 | /**
|
---|
| 294 | * Check whether user is about to operate on data outside of the download area.
|
---|
[4458] | 295 | * Request confirmation if he is.
|
---|
| 296 | *
|
---|
[4461] | 297 | * @param operation the operation name which is used for setting some preferences
|
---|
| 298 | * @param dialogTitle the title of the dialog being displayed
|
---|
| 299 | * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
|
---|
| 300 | * @param incompleteDialogMessage the message text to be displayed when data is incomplete
|
---|
[4458] | 301 | * @param primitives the primitives to operate on
|
---|
[4461] | 302 | * @param ignore {@code null} or a primitive to be ignored
|
---|
| 303 | * @return true, if operating on outlying primitives is OK; false, otherwise
|
---|
[4458] | 304 | */
|
---|
| 305 | public static boolean checkAndConfirmOutlyingOperation(String operation,
|
---|
| 306 | String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
|
---|
[6639] | 307 | Collection<? extends OsmPrimitive> primitives,
|
---|
[5060] | 308 | Collection<? extends OsmPrimitive> ignore) {
|
---|
[10970] | 309 | int checkRes = checkOutlyingOrIncompleteOperation(primitives, ignore);
|
---|
[10948] | 310 | if ((checkRes & IS_OUTSIDE) != 0) {
|
---|
[4458] | 311 | JPanel msg = new JPanel(new GridBagLayout());
|
---|
[6901] | 312 | msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>"));
|
---|
[4458] | 313 | boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
|
---|
| 314 | operation + "_outside_nodes",
|
---|
| 315 | Main.parent,
|
---|
| 316 | msg,
|
---|
| 317 | dialogTitle,
|
---|
| 318 | JOptionPane.YES_NO_OPTION,
|
---|
| 319 | JOptionPane.QUESTION_MESSAGE,
|
---|
| 320 | JOptionPane.YES_OPTION);
|
---|
[8510] | 321 | if (!answer)
|
---|
[4458] | 322 | return false;
|
---|
| 323 | }
|
---|
[10948] | 324 | if ((checkRes & IS_INCOMPLETE) != 0) {
|
---|
[4458] | 325 | JPanel msg = new JPanel(new GridBagLayout());
|
---|
[6901] | 326 | msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>"));
|
---|
[4458] | 327 | boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
|
---|
| 328 | operation + "_incomplete",
|
---|
| 329 | Main.parent,
|
---|
| 330 | msg,
|
---|
| 331 | dialogTitle,
|
---|
| 332 | JOptionPane.YES_NO_OPTION,
|
---|
| 333 | JOptionPane.QUESTION_MESSAGE,
|
---|
| 334 | JOptionPane.YES_OPTION);
|
---|
[8510] | 335 | if (!answer)
|
---|
[4458] | 336 | return false;
|
---|
| 337 | }
|
---|
| 338 | return true;
|
---|
| 339 | }
|
---|
| 340 |
|
---|
[12348] | 341 | /**
|
---|
| 342 | * Ensures that all primitives that are participating in this command belong to the affected data set.
|
---|
| 343 | *
|
---|
| 344 | * Commands may use this in their update methods to check the consitency of the primitives they operate on.
|
---|
| 345 | * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
|
---|
| 346 | */
|
---|
| 347 | protected void ensurePrimitivesAreInDataset() {
|
---|
| 348 | for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
|
---|
| 349 | if (primitive.getDataSet() != this.getAffectedDataSet()) {
|
---|
| 350 | throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
|
---|
| 351 | }
|
---|
| 352 | }
|
---|
| 353 | }
|
---|
| 354 |
|
---|
[8447] | 355 | @Override
|
---|
| 356 | public int hashCode() {
|
---|
[11243] | 357 | return Objects.hash(cloneMap, layer, data);
|
---|
[8447] | 358 | }
|
---|
| 359 |
|
---|
| 360 | @Override
|
---|
| 361 | public boolean equals(Object obj) {
|
---|
[9371] | 362 | if (this == obj) return true;
|
---|
| 363 | if (obj == null || getClass() != obj.getClass()) return false;
|
---|
| 364 | Command command = (Command) obj;
|
---|
| 365 | return Objects.equals(cloneMap, command.cloneMap) &&
|
---|
[11240] | 366 | Objects.equals(layer, command.layer) &&
|
---|
| 367 | Objects.equals(data, command.data);
|
---|
[8447] | 368 | }
|
---|
[10452] | 369 |
|
---|
| 370 | /**
|
---|
| 371 | * Invalidate all layers that were affected by this command.
|
---|
| 372 | * @see Layer#invalidate()
|
---|
| 373 | */
|
---|
| 374 | public void invalidateAffectedLayers() {
|
---|
| 375 | OsmDataLayer layer = getLayer();
|
---|
| 376 | if (layer != null) {
|
---|
| 377 | layer.invalidate();
|
---|
| 378 | }
|
---|
| 379 | }
|
---|
[21] | 380 | }
|
---|