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