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