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

Last change on this file was 19410, checked in by GerdP, 5 months ago

fix #24329:Upload Selection action not enabled after update

  • add listener so that the action is enabled after the selected object was changed
  • Property svn:eol-style set to native
File size: 13.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.JOptionPane;
17import javax.swing.SwingUtilities;
18
19import org.openstreetmap.josm.data.APIDataSet;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.Relation;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
27import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
28import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
29import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
30import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
31import org.openstreetmap.josm.gui.MainApplication;
32import org.openstreetmap.josm.gui.Notification;
33import org.openstreetmap.josm.gui.PleaseWaitRunnable;
34import org.openstreetmap.josm.gui.io.UploadSelectionDialog;
35import org.openstreetmap.josm.gui.layer.OsmDataLayer;
36import org.openstreetmap.josm.io.OsmServerBackreferenceReader;
37import org.openstreetmap.josm.io.OsmTransferException;
38import org.openstreetmap.josm.tools.CheckParameterUtil;
39import org.openstreetmap.josm.tools.ExceptionUtil;
40import org.openstreetmap.josm.tools.Shortcut;
41import org.openstreetmap.josm.tools.Utils;
42import org.xml.sax.SAXException;
43
44/**
45 * Uploads the current selection to the server.
46 * @since 2250
47 */
48public class UploadSelectionAction extends AbstractUploadAction implements DataSetListenerAdapter.Listener {
49 private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
50
51 /**
52 * Constructs a new {@code UploadSelectionAction}.
53 */
54 public UploadSelectionAction() {
55 super(
56 tr("Upload selection..."),
57 "uploadselection",
58 tr("Upload all changes in the current selection to the OSM server."),
59 // CHECKSTYLE.OFF: LineLength
60 Shortcut.registerShortcut("file:uploadSelection", tr("File: {0}", tr("Upload selection")), KeyEvent.VK_U, Shortcut.ALT_CTRL_SHIFT),
61 // CHECKSTYLE.ON: LineLength
62 true);
63 setHelpId(ht("/Action/UploadSelection"));
64 DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IMMEDIATELY);
65 }
66
67 @Override
68 protected void updateEnabledState() {
69 updateEnabledStateOnCurrentSelection();
70 }
71
72 @Override
73 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
74 updateEnabledStateOnModifiableSelection(selection);
75 OsmDataLayer editLayer = getLayerManager().getEditLayer();
76 if (isEnabled() && editLayer != null && !editLayer.isUploadable()) {
77 setEnabled(false);
78 }
79 if (isEnabled() && selection.parallelStream().noneMatch(OsmPrimitive::isModified)) {
80 setEnabled(false);
81 }
82 }
83
84 protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) {
85 return ds.allPrimitives().parallelStream()
86 .filter(p -> p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified())
87 .collect(Collectors.toSet());
88 }
89
90 protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) {
91 return primitives.parallelStream()
92 .filter(p -> p.isNewOrUndeleted() || (p.isModified() && !p.isIncomplete()))
93 .collect(Collectors.toSet());
94 }
95
96 @Override
97 public void actionPerformed(ActionEvent e) {
98 OsmDataLayer editLayer = getLayerManager().getEditLayer();
99 if (!isEnabled() || !editLayer.isUploadable())
100 return;
101 if (editLayer.isUploadDiscouraged() && UploadAction.warnUploadDiscouraged(editLayer)) {
102 return;
103 }
104 Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(editLayer.data.getAllSelected());
105 Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(editLayer.getDataSet());
106 if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) {
107 new Notification(tr("No changes to upload.")).setIcon(JOptionPane.INFORMATION_MESSAGE).show();
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 new Notification(tr("No changes to upload.")).setIcon(JOptionPane.INFORMATION_MESSAGE).show();
121 return;
122 }
123 uploadPrimitives(editLayer, toUpload);
124 }
125
126 /**
127 * Replies true if there is at least one non-new, deleted primitive in
128 * <code>primitives</code>
129 *
130 * @param primitives the primitives to scan
131 * @return true if there is at least one non-new, deleted primitive in
132 * <code>primitives</code>
133 */
134 protected boolean hasPrimitivesToDelete(Collection<OsmPrimitive> primitives) {
135 return primitives.parallelStream().anyMatch(p -> p.isDeleted() && p.isModified() && !p.isNew());
136 }
137
138 /**
139 * Uploads the primitives in <code>toUpload</code> to the server. Only
140 * uploads primitives which are either new, modified or deleted.
141 *
142 * Also checks whether <code>toUpload</code> has to be extended with
143 * deleted parents in order to avoid precondition violations on the server.
144 *
145 * @param layer the data layer from which we upload a subset of primitives
146 * @param toUpload the primitives to upload. If null or empty returns immediately
147 */
148 public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
149 if (Utils.isEmpty(toUpload)) return;
150 UploadHullBuilder builder = new UploadHullBuilder();
151 toUpload = builder.build(toUpload);
152 if (hasPrimitivesToDelete(toUpload)) {
153 // runs the check for deleted parents and then invokes
154 // processPostParentChecker()
155 //
156 MainApplication.worker.submit(new DeletedParentsChecker(layer, toUpload));
157 } else {
158 processPostParentChecker(layer, toUpload);
159 }
160 }
161
162 protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
163 APIDataSet ds = new APIDataSet(toUpload);
164 UploadAction action = new UploadAction();
165 action.uploadData(layer, ds);
166 }
167
168 /**
169 * Computes the collection of primitives to upload, given a collection of candidate
170 * primitives.
171 * Some of the candidates are excluded, i.e. if they aren't modified.
172 * Other primitives are added. A typical case is a primitive which is new and and
173 * which is referred by a modified relation. In order to upload the relation the
174 * new primitive has to be uploaded as well, even if it isn't included in the
175 * list of candidate primitives.
176 *
177 */
178 static class UploadHullBuilder implements OsmPrimitiveVisitor {
179 private Set<OsmPrimitive> hull;
180
181 UploadHullBuilder() {
182 hull = new HashSet<>();
183 }
184
185 @Override
186 public void visit(Node n) {
187 if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) {
188 // upload new nodes as well as modified and deleted ones
189 hull.add(n);
190 }
191 }
192
193 @Override
194 public void visit(Way w) {
195 if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) {
196 // upload new ways as well as modified and deleted ones
197 hull.add(w);
198 for (Node n: w.getNodes()) {
199 // we upload modified nodes even if they aren't in the current selection.
200 n.accept(this);
201 }
202 }
203 }
204
205 @Override
206 public void visit(Relation r) {
207 if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) {
208 hull.add(r);
209 for (OsmPrimitive p : r.getMemberPrimitives()) {
210 // add new relation members. Don't include modified
211 // relation members. r shouldn't refer to deleted primitives,
212 // so wont check here for deleted primitives here
213 //
214 if (p.isNewOrUndeleted()) {
215 p.accept(this);
216 }
217 }
218 }
219 }
220
221 /**
222 * Builds the "hull" of primitives to be uploaded given a base collection
223 * of osm primitives.
224 *
225 * @param base the base collection. Must not be null.
226 * @return the "hull"
227 * @throws IllegalArgumentException if base is null
228 */
229 public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) {
230 CheckParameterUtil.ensureParameterNotNull(base, "base");
231 hull = new HashSet<>();
232 for (OsmPrimitive p: base) {
233 p.accept(this);
234 }
235 return hull;
236 }
237 }
238
239 class DeletedParentsChecker extends PleaseWaitRunnable {
240 private boolean canceled;
241 private Exception lastException;
242 private final Collection<OsmPrimitive> toUpload;
243 private final OsmDataLayer layer;
244 private OsmServerBackreferenceReader reader;
245
246 /**
247 * Constructs a new {@code DeletedParentsChecker}.
248 * @param layer the data layer for which a collection of selected primitives is uploaded
249 * @param toUpload the collection of primitives to upload
250 */
251 DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
252 super(tr("Checking parents for deleted objects"));
253 this.toUpload = toUpload;
254 this.layer = layer;
255 }
256
257 @Override
258 protected void cancel() {
259 this.canceled = true;
260 synchronized (this) {
261 if (reader != null) {
262 reader.cancel();
263 }
264 }
265 }
266
267 @Override
268 protected void finish() {
269 if (canceled)
270 return;
271 if (lastException != null) {
272 ExceptionUtil.explainException(lastException);
273 return;
274 }
275 SwingUtilities.invokeLater(() -> processPostParentChecker(layer, toUpload));
276 }
277
278 /**
279 * Replies the collection of deleted OSM primitives for which we have to check whether
280 * there are dangling references on the server.
281 *
282 * @return primitives to check
283 */
284 protected Set<OsmPrimitive> getPrimitivesToCheckForParents() {
285 return toUpload.parallelStream().filter(p -> p.isDeleted() && !p.isNewOrUndeleted()).collect(Collectors.toSet());
286 }
287
288 @Override
289 protected void realRun() throws SAXException, IOException, OsmTransferException {
290 try {
291 Stack<OsmPrimitive> toCheck = new Stack<>();
292 toCheck.addAll(getPrimitivesToCheckForParents());
293 Set<OsmPrimitive> checked = new HashSet<>();
294 while (!toCheck.isEmpty()) {
295 if (canceled) return;
296 OsmPrimitive current = toCheck.pop();
297 synchronized (this) {
298 reader = new OsmServerBackreferenceReader(current).setAllowIncompleteParentWays(true);
299 }
300 getProgressMonitor().subTask(tr("Reading parents of ''{0}''", current.getDisplayName(DefaultNameFormatter.getInstance())));
301 DataSet ds = reader.parseOsm(getProgressMonitor().createSubTaskMonitor(1, false));
302 synchronized (this) {
303 reader = null;
304 }
305 checked.add(current);
306 getProgressMonitor().subTask(tr("Checking for deleted parents in the local dataset"));
307 for (OsmPrimitive p: ds.allPrimitives()) {
308 if (canceled) return;
309 if (p instanceof Node || (p instanceof Way && !(current instanceof Node))) continue;
310 OsmPrimitive myDeletedParent = layer.data.getPrimitiveById(p);
311 // our local dataset includes a deleted parent of a primitive we want
312 // to delete. Include this parent in the collection of uploaded primitives
313 if (myDeletedParent != null && myDeletedParent.isDeleted()) {
314 if (!toUpload.contains(myDeletedParent)) {
315 toUpload.add(myDeletedParent);
316 }
317 if (!checked.contains(myDeletedParent)) {
318 toCheck.push(myDeletedParent);
319 }
320 }
321 }
322 }
323 } catch (OsmTransferException e) {
324 if (canceled)
325 // ignore exception
326 return;
327 lastException = e;
328 }
329 }
330 }
331
332 @Override
333 public void destroy() {
334 super.destroy();
335 DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
336
337 }
338
339 @Override
340 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
341 updateEnabledStateOnCurrentSelection();
342 }
343}
Note: See TracBrowser for help on using the repository browser.