001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.io.IOException; 010import java.util.Collection; 011import java.util.HashSet; 012import java.util.Set; 013import java.util.Stack; 014 015import javax.swing.JOptionPane; 016import javax.swing.SwingUtilities; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.data.APIDataSet; 020import org.openstreetmap.josm.data.osm.Changeset; 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.Relation; 025import org.openstreetmap.josm.data.osm.Way; 026import org.openstreetmap.josm.data.osm.visitor.Visitor; 027import org.openstreetmap.josm.gui.DefaultNameFormatter; 028import org.openstreetmap.josm.gui.PleaseWaitRunnable; 029import org.openstreetmap.josm.gui.io.UploadSelectionDialog; 030import org.openstreetmap.josm.gui.layer.OsmDataLayer; 031import org.openstreetmap.josm.io.OsmServerBackreferenceReader; 032import org.openstreetmap.josm.io.OsmTransferException; 033import org.openstreetmap.josm.tools.CheckParameterUtil; 034import org.openstreetmap.josm.tools.ExceptionUtil; 035import org.openstreetmap.josm.tools.Shortcut; 036import org.xml.sax.SAXException; 037 038/** 039 * Uploads the current selection to the server. 040 * 041 */ 042public class UploadSelectionAction extends JosmAction{ 043 public UploadSelectionAction() { 044 super( 045 tr("Upload selection"), 046 "uploadselection", 047 tr("Upload all changes in the current selection to the OSM server."), 048 Shortcut.registerShortcut("file:uploadSelection", tr("File: {0}", tr("Upload selection")), KeyEvent.VK_U, Shortcut.ALT_CTRL_SHIFT), 049 true); 050 putValue("help", ht("/Action/UploadSelection")); 051 } 052 053 @Override 054 protected void updateEnabledState() { 055 if (getCurrentDataSet() == null) { 056 setEnabled(false); 057 } else { 058 updateEnabledState(getCurrentDataSet().getAllSelected()); 059 } 060 } 061 062 @Override 063 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 064 setEnabled(selection != null && !selection.isEmpty()); 065 } 066 067 protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) { 068 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>(); 069 for (OsmPrimitive p: ds.allPrimitives()) { 070 if (p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified()) { 071 ret.add(p); 072 } 073 } 074 return ret; 075 } 076 077 protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) { 078 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>(); 079 for (OsmPrimitive p: primitives) { 080 if (p.isNewOrUndeleted()) { 081 ret.add(p); 082 } else if (p.isModified() && !p.isIncomplete()) { 083 ret.add(p); 084 } 085 } 086 return ret; 087 } 088 089 @Override 090 public void actionPerformed(ActionEvent e) { 091 if (!isEnabled()) 092 return; 093 if (getEditLayer().isUploadDiscouraged()) { 094 if (UploadAction.warnUploadDiscouraged(getEditLayer())) { 095 return; 096 } 097 } 098 UploadHullBuilder builder = new UploadHullBuilder(); 099 UploadSelectionDialog dialog = new UploadSelectionDialog(); 100 Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(getEditLayer().data.getAllSelected()); 101 Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(getEditLayer().data); 102 if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) { 103 JOptionPane.showMessageDialog( 104 Main.parent, 105 tr("No changes to upload."), 106 tr("Warning"), 107 JOptionPane.INFORMATION_MESSAGE 108 ); 109 return; 110 } 111 dialog.populate( 112 modifiedCandidates, 113 deletedCandidates 114 ); 115 dialog.setVisible(true); 116 if (dialog.isCanceled()) 117 return; 118 Collection<OsmPrimitive> toUpload = builder.build(dialog.getSelectedPrimitives()); 119 if (toUpload.isEmpty()) { 120 JOptionPane.showMessageDialog( 121 Main.parent, 122 tr("No changes to upload."), 123 tr("Warning"), 124 JOptionPane.INFORMATION_MESSAGE 125 ); 126 return; 127 } 128 uploadPrimitives(getEditLayer(), toUpload); 129 } 130 131 /** 132 * Replies true if there is at least one non-new, deleted primitive in 133 * <code>primitives</code> 134 * 135 * @param primitives the primitives to scan 136 * @return true if there is at least one non-new, deleted primitive in 137 * <code>primitives</code> 138 */ 139 protected boolean hasPrimitivesToDelete(Collection<OsmPrimitive> primitives) { 140 for (OsmPrimitive p: primitives) 141 if (p.isDeleted() && p.isModified() && !p.isNew()) 142 return true; 143 return false; 144 } 145 146 /** 147 * Uploads the primitives in <code>toUpload</code> to the server. Only 148 * uploads primitives which are either new, modified or deleted. 149 * 150 * Also checks whether <code>toUpload</code> has to be extended with 151 * deleted parents in order to avoid precondition violations on the server. 152 * 153 * @param layer the data layer from which we upload a subset of primitives 154 * @param toUpload the primitives to upload. If null or empty returns immediatelly 155 */ 156 public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 157 if (toUpload == null || toUpload.isEmpty()) return; 158 UploadHullBuilder builder = new UploadHullBuilder(); 159 toUpload = builder.build(toUpload); 160 if (hasPrimitivesToDelete(toUpload)) { 161 // runs the check for deleted parents and then invokes 162 // processPostParentChecker() 163 // 164 Main.worker.submit(new DeletedParentsChecker(layer, toUpload)); 165 } else { 166 processPostParentChecker(layer, toUpload); 167 } 168 } 169 170 protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 171 APIDataSet ds = new APIDataSet(toUpload); 172 UploadAction action = new UploadAction(); 173 action.uploadData(layer, ds); 174 } 175 176 /** 177 * Computes the collection of primitives to upload, given a collection of candidate 178 * primitives. 179 * Some of the candidates are excluded, i.e. if they aren't modified. 180 * Other primitives are added. A typical case is a primitive which is new and and 181 * which is referred by a modified relation. In order to upload the relation the 182 * new primitive has to be uploaded as well, even if it isn't included in the 183 * list of candidate primitives. 184 * 185 */ 186 static class UploadHullBuilder implements Visitor { 187 private Set<OsmPrimitive> hull; 188 189 public UploadHullBuilder(){ 190 hull = new HashSet<OsmPrimitive>(); 191 } 192 193 @Override 194 public void visit(Node n) { 195 if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) { 196 // upload new nodes as well as modified and deleted ones 197 hull.add(n); 198 } 199 } 200 201 @Override 202 public void visit(Way w) { 203 if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) { 204 // upload new ways as well as modified and deleted ones 205 hull.add(w); 206 for (Node n: w.getNodes()) { 207 // we upload modified nodes even if they aren't in the current 208 // selection. 209 n.accept(this); 210 } 211 } 212 } 213 214 @Override 215 public void visit(Relation r) { 216 if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) { 217 hull.add(r); 218 for (OsmPrimitive p : r.getMemberPrimitives()) { 219 // add new relation members. Don't include modified 220 // relation members. r shouldn't refer to deleted primitives, 221 // so wont check here for deleted primitives here 222 // 223 if (p.isNewOrUndeleted()) { 224 p.accept(this); 225 } 226 } 227 } 228 } 229 230 @Override 231 public void visit(Changeset cs) { 232 // do nothing 233 } 234 235 /** 236 * Builds the "hull" of primitives to be uploaded given a base collection 237 * of osm primitives. 238 * 239 * @param base the base collection. Must not be null. 240 * @return the "hull" 241 * @throws IllegalArgumentException thrown if base is null 242 */ 243 public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) throws IllegalArgumentException{ 244 CheckParameterUtil.ensureParameterNotNull(base, "base"); 245 hull = new HashSet<OsmPrimitive>(); 246 for (OsmPrimitive p: base) { 247 p.accept(this); 248 } 249 return hull; 250 } 251 } 252 253 class DeletedParentsChecker extends PleaseWaitRunnable { 254 private boolean canceled; 255 private Exception lastException; 256 private Collection<OsmPrimitive> toUpload; 257 private OsmDataLayer layer; 258 private OsmServerBackreferenceReader reader; 259 260 /** 261 * 262 * @param layer the data layer for which a collection of selected primitives is uploaded 263 * @param toUpload the collection of primitives to upload 264 */ 265 public DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 266 super(tr("Checking parents for deleted objects")); 267 this.toUpload = toUpload; 268 this.layer = layer; 269 } 270 271 @Override 272 protected void cancel() { 273 this.canceled = true; 274 synchronized (this) { 275 if (reader != null) { 276 reader.cancel(); 277 } 278 } 279 } 280 281 @Override 282 protected void finish() { 283 if (canceled) 284 return; 285 if (lastException != null) { 286 ExceptionUtil.explainException(lastException); 287 return; 288 } 289 Runnable r = new Runnable() { 290 @Override 291 public void run() { 292 processPostParentChecker(layer, toUpload); 293 } 294 }; 295 SwingUtilities.invokeLater(r); 296 } 297 298 /** 299 * Replies the collection of deleted OSM primitives for which we have to check whether 300 * there are dangling references on the server. 301 * 302 * @return primitives to check 303 */ 304 protected Set<OsmPrimitive> getPrimitivesToCheckForParents() { 305 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>(); 306 for (OsmPrimitive p: toUpload) { 307 if (p.isDeleted() && !p.isNewOrUndeleted()) { 308 ret.add(p); 309 } 310 } 311 return ret; 312 } 313 314 @Override 315 protected void realRun() throws SAXException, IOException, OsmTransferException { 316 try { 317 Stack<OsmPrimitive> toCheck = new Stack<OsmPrimitive>(); 318 toCheck.addAll(getPrimitivesToCheckForParents()); 319 Set<OsmPrimitive> checked = new HashSet<OsmPrimitive>(); 320 while(!toCheck.isEmpty()) { 321 if (canceled) return; 322 OsmPrimitive current = toCheck.pop(); 323 synchronized(this) { 324 reader = new OsmServerBackreferenceReader(current); 325 } 326 getProgressMonitor().subTask(tr("Reading parents of ''{0}''", current.getDisplayName(DefaultNameFormatter.getInstance()))); 327 DataSet ds = reader.parseOsm(getProgressMonitor().createSubTaskMonitor(1, false)); 328 synchronized(this) { 329 reader = null; 330 } 331 checked.add(current); 332 getProgressMonitor().subTask(tr("Checking for deleted parents in the local dataset")); 333 for (OsmPrimitive p: ds.allPrimitives()) { 334 if (canceled) return; 335 OsmPrimitive myDeletedParent = layer.data.getPrimitiveById(p); 336 // our local dataset includes a deleted parent of a primitive we want 337 // to delete. Include this parent in the collection of uploaded primitives 338 // 339 if (myDeletedParent != null && myDeletedParent.isDeleted()) { 340 if (!toUpload.contains(myDeletedParent)) { 341 toUpload.add(myDeletedParent); 342 } 343 if (!checked.contains(myDeletedParent)) { 344 toCheck.push(myDeletedParent); 345 } 346 } 347 } 348 } 349 } catch(Exception e) { 350 if (canceled) 351 // ignore exception 352 return; 353 lastException = e; 354 } 355 } 356 } 357}