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