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