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