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