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