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