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; 011 012import javax.swing.JOptionPane; 013import javax.swing.JPanel; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.data.coor.EastNorth; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.PrimitiveData; 021import org.openstreetmap.josm.data.osm.Relation; 022import org.openstreetmap.josm.data.osm.Way; 023import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 024import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 025import org.openstreetmap.josm.gui.layer.Layer; 026import org.openstreetmap.josm.gui.layer.OsmDataLayer; 027import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 028import org.openstreetmap.josm.tools.CheckParameterUtil; 029 030/** 031 * Classes implementing Command modify a dataset in a specific way. A command is 032 * one atomic action on a specific dataset, such as move or delete. 033 * 034 * The command remembers the {@link OsmDataLayer} it is operating on. 035 * 036 * @author imi 037 */ 038public abstract class Command extends PseudoCommand { 039 040 private static final class CloneVisitor extends AbstractVisitor { 041 public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>(); 042 043 @Override 044 public void visit(Node n) { 045 orig.put(n, n.save()); 046 } 047 @Override 048 public void visit(Way w) { 049 orig.put(w, w.save()); 050 } 051 @Override 052 public void visit(Relation e) { 053 orig.put(e, e.save()); 054 } 055 } 056 057 /** 058 * Small helper for holding the interesting part of the old data state of the objects. 059 */ 060 public static class OldNodeState { 061 062 final LatLon latlon; 063 final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement 064 final boolean modified; 065 066 /** 067 * Constructs a new {@code OldNodeState} for the given node. 068 * @param node The node whose state has to be remembered 069 */ 070 public OldNodeState(Node node){ 071 latlon = node.getCoor(); 072 eastNorth = node.getEastNorth(); 073 modified = node.isModified(); 074 } 075 } 076 077 /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */ 078 private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>(); 079 080 /** the layer which this command is applied to */ 081 private final OsmDataLayer layer; 082 083 /** 084 * Creates a new command in the context of the current edit layer, if any 085 */ 086 public Command() { 087 this.layer = Main.main == null ? null : Main.main.getEditLayer(); 088 } 089 090 /** 091 * Creates a new command in the context of a specific data layer 092 * 093 * @param layer the data layer. Must not be null. 094 * @throws IllegalArgumentException thrown if layer is null 095 */ 096 public Command(OsmDataLayer layer) throws IllegalArgumentException { 097 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 098 this.layer = layer; 099 } 100 101 /** 102 * Executes the command on the dataset. This implementation will remember all 103 * primitives returned by fillModifiedData for restoring them on undo. 104 * @return true 105 */ 106 public boolean executeCommand() { 107 CloneVisitor visitor = new CloneVisitor(); 108 Collection<OsmPrimitive> all = new ArrayList<>(); 109 fillModifiedData(all, all, all); 110 for (OsmPrimitive osm : all) { 111 osm.accept(visitor); 112 } 113 cloneMap = visitor.orig; 114 return true; 115 } 116 117 /** 118 * Undoes the command. 119 * It can be assumed that all objects are in the same state they were before. 120 * It can also be assumed that executeCommand was called exactly once before. 121 * 122 * This implementation undoes all objects stored by a former call to executeCommand. 123 */ 124 public void undoCommand() { 125 for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) { 126 OsmPrimitive primitive = e.getKey(); 127 if (primitive.getDataSet() != null) { 128 e.getKey().load(e.getValue()); 129 } 130 } 131 } 132 133 /** 134 * Called when a layer has been removed to have the command remove itself from 135 * any buffer if it is not longer applicable to the dataset (e.g. it was part of 136 * the removed layer) 137 * 138 * @param oldLayer the old layer 139 * @return true if this command 140 */ 141 public boolean invalidBecauselayerRemoved(Layer oldLayer) { 142 if (!(oldLayer instanceof OsmDataLayer)) 143 return false; 144 return layer == oldLayer; 145 } 146 147 /** 148 * Lets other commands access the original version 149 * of the object. Usually for undoing. 150 * @param osm The requested OSM object 151 * @return The original version of the requested object, if any 152 */ 153 public PrimitiveData getOrig(OsmPrimitive osm) { 154 return cloneMap.get(osm); 155 } 156 157 /** 158 * Replies the layer this command is (or was) applied to. 159 * 160 */ 161 protected OsmDataLayer getLayer() { 162 return layer; 163 } 164 165 /** 166 * Fill in the changed data this command operates on. 167 * Add to the lists, don't clear them. 168 * 169 * @param modified The modified primitives 170 * @param deleted The deleted primitives 171 * @param added The added primitives 172 */ 173 public abstract void fillModifiedData(Collection<OsmPrimitive> modified, 174 Collection<OsmPrimitive> deleted, 175 Collection<OsmPrimitive> added); 176 177 /** 178 * Return the primitives that take part in this command. 179 */ 180 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 181 return cloneMap.keySet(); 182 } 183 184 /** 185 * Check whether user is about to operate on data outside of the download area. 186 * Request confirmation if he is. 187 * 188 * @param operation the operation name which is used for setting some preferences 189 * @param dialogTitle the title of the dialog being displayed 190 * @param outsideDialogMessage the message text to be displayed when data is outside of the download area 191 * @param incompleteDialogMessage the message text to be displayed when data is incomplete 192 * @param primitives the primitives to operate on 193 * @param ignore {@code null} or a primitive to be ignored 194 * @return true, if operating on outlying primitives is OK; false, otherwise 195 */ 196 public static boolean checkAndConfirmOutlyingOperation(String operation, 197 String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, 198 Collection<? extends OsmPrimitive> primitives, 199 Collection<? extends OsmPrimitive> ignore) { 200 boolean outside = false; 201 boolean incomplete = false; 202 for (OsmPrimitive osm : primitives) { 203 if (osm.isIncomplete()) { 204 incomplete = true; 205 } else if (osm.isOutsideDownloadArea() 206 && (ignore == null || !ignore.contains(osm))) { 207 outside = true; 208 } 209 } 210 if (outside) { 211 JPanel msg = new JPanel(new GridBagLayout()); 212 msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>")); 213 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 214 operation + "_outside_nodes", 215 Main.parent, 216 msg, 217 dialogTitle, 218 JOptionPane.YES_NO_OPTION, 219 JOptionPane.QUESTION_MESSAGE, 220 JOptionPane.YES_OPTION); 221 if(!answer) 222 return false; 223 } 224 if (incomplete) { 225 JPanel msg = new JPanel(new GridBagLayout()); 226 msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>")); 227 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 228 operation + "_incomplete", 229 Main.parent, 230 msg, 231 dialogTitle, 232 JOptionPane.YES_NO_OPTION, 233 JOptionPane.QUESTION_MESSAGE, 234 JOptionPane.YES_OPTION); 235 if(!answer) 236 return false; 237 } 238 return true; 239 } 240 241}