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 updateEnabledStateOnCurrentSelection(); 061 } 062 063 @Override 064 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 065 setEnabled(selection != null && !selection.isEmpty()); 066 } 067 068 protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) { 069 Set<OsmPrimitive> ret = new HashSet<>(); 070 for (OsmPrimitive p: ds.allPrimitives()) { 071 if (p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified()) { 072 ret.add(p); 073 } 074 } 075 return ret; 076 } 077 078 protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) { 079 Set<OsmPrimitive> ret = new HashSet<>(); 080 for (OsmPrimitive p: primitives) { 081 if (p.isNewOrUndeleted() || (p.isModified() && !p.isIncomplete())) { 082 ret.add(p); 083 } 084 } 085 return ret; 086 } 087 088 @Override 089 public void actionPerformed(ActionEvent e) { 090 OsmDataLayer editLayer = getLayerManager().getEditLayer(); 091 if (!isEnabled()) 092 return; 093 if (editLayer.isUploadDiscouraged() && UploadAction.warnUploadDiscouraged(editLayer)) { 094 return; 095 } 096 Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(editLayer.data.getAllSelected()); 097 Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(editLayer.data); 098 if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) { 099 JOptionPane.showMessageDialog( 100 Main.parent, 101 tr("No changes to upload."), 102 tr("Warning"), 103 JOptionPane.INFORMATION_MESSAGE 104 ); 105 return; 106 } 107 UploadSelectionDialog dialog = new UploadSelectionDialog(); 108 dialog.populate( 109 modifiedCandidates, 110 deletedCandidates 111 ); 112 dialog.setVisible(true); 113 if (dialog.isCanceled()) 114 return; 115 Collection<OsmPrimitive> toUpload = new UploadHullBuilder().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(editLayer, 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 } 141 return false; 142 } 143 144 /** 145 * Uploads the primitives in <code>toUpload</code> to the server. Only 146 * uploads primitives which are either new, modified or deleted. 147 * 148 * Also checks whether <code>toUpload</code> has to be extended with 149 * deleted parents in order to avoid precondition violations on the server. 150 * 151 * @param layer the data layer from which we upload a subset of primitives 152 * @param toUpload the primitives to upload. If null or empty returns immediatelly 153 */ 154 public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 155 if (toUpload == null || toUpload.isEmpty()) return; 156 UploadHullBuilder builder = new UploadHullBuilder(); 157 toUpload = builder.build(toUpload); 158 if (hasPrimitivesToDelete(toUpload)) { 159 // runs the check for deleted parents and then invokes 160 // processPostParentChecker() 161 // 162 Main.worker.submit(new DeletedParentsChecker(layer, toUpload)); 163 } else { 164 processPostParentChecker(layer, toUpload); 165 } 166 } 167 168 protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 169 APIDataSet ds = new APIDataSet(toUpload); 170 UploadAction action = new UploadAction(); 171 action.uploadData(layer, ds); 172 } 173 174 /** 175 * Computes the collection of primitives to upload, given a collection of candidate 176 * primitives. 177 * Some of the candidates are excluded, i.e. if they aren't modified. 178 * Other primitives are added. A typical case is a primitive which is new and and 179 * which is referred by a modified relation. In order to upload the relation the 180 * new primitive has to be uploaded as well, even if it isn't included in the 181 * list of candidate primitives. 182 * 183 */ 184 static class UploadHullBuilder implements Visitor { 185 private Set<OsmPrimitive> hull; 186 187 UploadHullBuilder() { 188 hull = new HashSet<>(); 189 } 190 191 @Override 192 public void visit(Node n) { 193 if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) { 194 // upload new nodes as well as modified and deleted ones 195 hull.add(n); 196 } 197 } 198 199 @Override 200 public void visit(Way w) { 201 if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) { 202 // upload new ways as well as modified and deleted ones 203 hull.add(w); 204 for (Node n: w.getNodes()) { 205 // we upload modified nodes even if they aren't in the current 206 // selection. 207 n.accept(this); 208 } 209 } 210 } 211 212 @Override 213 public void visit(Relation r) { 214 if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) { 215 hull.add(r); 216 for (OsmPrimitive p : r.getMemberPrimitives()) { 217 // add new relation members. Don't include modified 218 // relation members. r shouldn't refer to deleted primitives, 219 // so wont check here for deleted primitives here 220 // 221 if (p.isNewOrUndeleted()) { 222 p.accept(this); 223 } 224 } 225 } 226 } 227 228 @Override 229 public void visit(Changeset cs) { 230 // do nothing 231 } 232 233 /** 234 * Builds the "hull" of primitives to be uploaded given a base collection 235 * of osm primitives. 236 * 237 * @param base the base collection. Must not be null. 238 * @return the "hull" 239 * @throws IllegalArgumentException if base is null 240 */ 241 public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) { 242 CheckParameterUtil.ensureParameterNotNull(base, "base"); 243 hull = new HashSet<>(); 244 for (OsmPrimitive p: base) { 245 p.accept(this); 246 } 247 return hull; 248 } 249 } 250 251 class DeletedParentsChecker extends PleaseWaitRunnable { 252 private boolean canceled; 253 private Exception lastException; 254 private final Collection<OsmPrimitive> toUpload; 255 private final OsmDataLayer layer; 256 private OsmServerBackreferenceReader reader; 257 258 /** 259 * 260 * @param layer the data layer for which a collection of selected primitives is uploaded 261 * @param toUpload the collection of primitives to upload 262 */ 263 DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) { 264 super(tr("Checking parents for deleted objects")); 265 this.toUpload = toUpload; 266 this.layer = layer; 267 } 268 269 @Override 270 protected void cancel() { 271 this.canceled = true; 272 synchronized (this) { 273 if (reader != null) { 274 reader.cancel(); 275 } 276 } 277 } 278 279 @Override 280 protected void finish() { 281 if (canceled) 282 return; 283 if (lastException != null) { 284 ExceptionUtil.explainException(lastException); 285 return; 286 } 287 SwingUtilities.invokeLater(() -> processPostParentChecker(layer, toUpload)); 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 primitives to check 295 */ 296 protected Set<OsmPrimitive> getPrimitivesToCheckForParents() { 297 Set<OsmPrimitive> ret = new HashSet<>(); 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<>(); 310 toCheck.addAll(getPrimitivesToCheckForParents()); 311 Set<OsmPrimitive> checked = new HashSet<>(); 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 if (myDeletedParent != null && myDeletedParent.isDeleted()) { 331 if (!toUpload.contains(myDeletedParent)) { 332 toUpload.add(myDeletedParent); 333 } 334 if (!checked.contains(myDeletedParent)) { 335 toCheck.push(myDeletedParent); 336 } 337 } 338 } 339 } 340 } catch (OsmTransferException e) { 341 if (canceled) 342 // ignore exception 343 return; 344 lastException = e; 345 } 346 } 347 } 348}