source: josm/trunk/src/org/openstreetmap/josm/actions/UploadSelectionAction.java@ 17379

Last change on this file since 17379 was 15513, checked in by Don-vip, 4 years ago

fix #18296 - make sure "Upload selection" is always enabled if required after data change event

  • Property svn:eol-style set to native
File size: 12.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.io.IOException;
10import java.util.Collection;
11import java.util.HashSet;
12import java.util.Set;
13import java.util.Stack;
14import java.util.stream.Collectors;
15
16import javax.swing.SwingUtilities;
17
18import org.openstreetmap.josm.data.APIDataSet;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.Way;
25import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
26import org.openstreetmap.josm.gui.MainApplication;
27import org.openstreetmap.josm.gui.Notification;
28import org.openstreetmap.josm.gui.PleaseWaitRunnable;
29import org.openstreetmap.josm.gui.io.UploadSelectionDialog;
30import org.openstreetmap.josm.gui.layer.OsmDataLayer;
31import org.openstreetmap.josm.io.OsmServerBackreferenceReader;
32import org.openstreetmap.josm.io.OsmTransferException;
33import org.openstreetmap.josm.tools.CheckParameterUtil;
34import org.openstreetmap.josm.tools.ExceptionUtil;
35import org.openstreetmap.josm.tools.Shortcut;
36import org.xml.sax.SAXException;
37
38/**
39 * Uploads the current selection to the server.
40 * @since 2250
41 */
42public class UploadSelectionAction extends AbstractUploadAction {
43 /**
44 * Constructs a new {@code UploadSelectionAction}.
45 */
46 public UploadSelectionAction() {
47 super(
48 tr("Upload selection..."),
49 "uploadselection",
50 tr("Upload all changes in the current selection to the OSM server."),
51 // CHECKSTYLE.OFF: LineLength
52 Shortcut.registerShortcut("file:uploadSelection", tr("File: {0}", tr("Upload selection")), KeyEvent.VK_U, Shortcut.ALT_CTRL_SHIFT),
53 // CHECKSTYLE.ON: LineLength
54 true);
55 setHelpId(ht("/Action/UploadSelection"));
56 }
57
58 @Override
59 protected void updateEnabledState() {
60 updateEnabledStateOnCurrentSelection();
61 }
62
63 @Override
64 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
65 updateEnabledStateOnModifiableSelection(selection);
66 OsmDataLayer editLayer = getLayerManager().getEditLayer();
67 if (isEnabled() && editLayer != null && !editLayer.isUploadable()) {
68 setEnabled(false);
69 }
70 if (isEnabled() && selection.parallelStream().noneMatch(OsmPrimitive::isModified)) {
71 setEnabled(false);
72 }
73 }
74
75 protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) {
76 return ds.allPrimitives().parallelStream()
77 .filter(p -> p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified())
78 .collect(Collectors.toSet());
79 }
80
81 protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) {
82 return primitives.parallelStream()
83 .filter(p -> p.isNewOrUndeleted() || (p.isModified() && !p.isIncomplete()))
84 .collect(Collectors.toSet());
85 }
86
87 @Override
88 public void actionPerformed(ActionEvent e) {
89 OsmDataLayer editLayer = getLayerManager().getEditLayer();
90 if (!isEnabled() || !editLayer.isUploadable())
91 return;
92 if (editLayer.isUploadDiscouraged() && UploadAction.warnUploadDiscouraged(editLayer)) {
93 return;
94 }
95 Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(editLayer.data.getAllSelected());
96 Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(editLayer.getDataSet());
97 if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) {
98 new Notification(tr("No changes to upload.")).show();
99 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}
Note: See TracBrowser for help on using the repository browser.