001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.actions;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.I18n.tr;
006
007 import java.awt.event.ActionEvent;
008 import java.io.IOException;
009 import java.util.Collection;
010 import java.util.HashSet;
011 import java.util.Set;
012 import java.util.Stack;
013
014 import javax.swing.JOptionPane;
015 import javax.swing.SwingUtilities;
016
017 import org.openstreetmap.josm.Main;
018 import org.openstreetmap.josm.data.APIDataSet;
019 import org.openstreetmap.josm.data.osm.Changeset;
020 import org.openstreetmap.josm.data.osm.DataSet;
021 import org.openstreetmap.josm.data.osm.Node;
022 import org.openstreetmap.josm.data.osm.OsmPrimitive;
023 import org.openstreetmap.josm.data.osm.Relation;
024 import org.openstreetmap.josm.data.osm.Way;
025 import org.openstreetmap.josm.data.osm.visitor.Visitor;
026 import org.openstreetmap.josm.gui.DefaultNameFormatter;
027 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
028 import org.openstreetmap.josm.gui.io.UploadSelectionDialog;
029 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030 import org.openstreetmap.josm.io.OsmServerBackreferenceReader;
031 import org.openstreetmap.josm.io.OsmTransferException;
032 import org.openstreetmap.josm.tools.CheckParameterUtil;
033 import org.openstreetmap.josm.tools.ExceptionUtil;
034 import org.xml.sax.SAXException;
035
036 /**
037 * Uploads the current selection to the server.
038 *
039 */
040 public class UploadSelectionAction extends JosmAction{
041 public UploadSelectionAction() {
042 super(
043 tr("Upload selection"),
044 "uploadselection",
045 tr("Upload all changes in the current selection to the OSM server."),
046 null, /* no shortcut */
047 true);
048 putValue("help", ht("/Action/UploadSelection"));
049 }
050
051 @Override
052 protected void updateEnabledState() {
053 if (getCurrentDataSet() == null) {
054 setEnabled(false);
055 } else {
056 updateEnabledState(getCurrentDataSet().getAllSelected());
057 }
058 }
059
060 @Override
061 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
062 setEnabled(selection != null && !selection.isEmpty());
063 }
064
065 protected Set<OsmPrimitive> getDeletedPrimitives(DataSet ds) {
066 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
067 for (OsmPrimitive p: ds.allPrimitives()) {
068 if (p.isDeleted() && !p.isNew() && p.isVisible() && p.isModified()) {
069 ret.add(p);
070 }
071 }
072 return ret;
073 }
074
075 protected Set<OsmPrimitive> getModifiedPrimitives(Collection<OsmPrimitive> primitives) {
076 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
077 for (OsmPrimitive p: primitives) {
078 if (p.isNewOrUndeleted()) {
079 ret.add(p);
080 } else if (p.isModified() && !p.isIncomplete()) {
081 ret.add(p);
082 }
083 }
084 return ret;
085 }
086
087 public void actionPerformed(ActionEvent e) {
088 if (!isEnabled())
089 return;
090 if (getEditLayer().isUploadDiscouraged()) {
091 if (UploadAction.warnUploadDiscouraged(getEditLayer())) {
092 return;
093 }
094 }
095 UploadHullBuilder builder = new UploadHullBuilder();
096 UploadSelectionDialog dialog = new UploadSelectionDialog();
097 Collection<OsmPrimitive> modifiedCandidates = getModifiedPrimitives(getEditLayer().data.getAllSelected());
098 Collection<OsmPrimitive> deletedCandidates = getDeletedPrimitives(getEditLayer().data);
099 if (modifiedCandidates.isEmpty() && deletedCandidates.isEmpty()) {
100 JOptionPane.showMessageDialog(
101 Main.parent,
102 tr("No changes to upload."),
103 tr("Warning"),
104 JOptionPane.INFORMATION_MESSAGE
105 );
106 return;
107 }
108 dialog.populate(
109 modifiedCandidates,
110 deletedCandidates
111 );
112 dialog.setVisible(true);
113 if (dialog.isCanceled())
114 return;
115 Collection<OsmPrimitive> toUpload = builder.build(dialog.getSelectedPrimitives());
116 if (toUpload.isEmpty()) {
117 JOptionPane.showMessageDialog(
118 Main.parent,
119 tr("No changes to upload."),
120 tr("Warning"),
121 JOptionPane.INFORMATION_MESSAGE
122 );
123 return;
124 }
125 uploadPrimitives(getEditLayer(), toUpload);
126 }
127
128 /**
129 * Replies true if there is at least one non-new, deleted primitive in
130 * <code>primitives</code>
131 *
132 * @param primitives the primitives to scan
133 * @return true if there is at least one non-new, deleted primitive in
134 * <code>primitives</code>
135 */
136 protected boolean hasPrimitivesToDelete(Collection<OsmPrimitive> primitives) {
137 for (OsmPrimitive p: primitives)
138 if (p.isDeleted() && p.isModified() && !p.isNew())
139 return true;
140 return false;
141 }
142
143 /**
144 * Uploads the primitives in <code>toUpload</code> to the server. Only
145 * uploads primitives which are either new, modified or deleted.
146 *
147 * Also checks whether <code>toUpload</code> has to be extended with
148 * deleted parents in order to avoid precondition violations on the server.
149 *
150 * @param layer the data layer from which we upload a subset of primitives
151 * @param toUpload the primitives to upload. If null or empty returns immediatelly
152 */
153 public void uploadPrimitives(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
154 if (toUpload == null || toUpload.isEmpty()) return;
155 UploadHullBuilder builder = new UploadHullBuilder();
156 toUpload = builder.build(toUpload);
157 if (hasPrimitivesToDelete(toUpload)) {
158 // runs the check for deleted parents and then invokes
159 // processPostParentChecker()
160 //
161 Main.worker.submit(new DeletedParentsChecker(layer, toUpload));
162 } else {
163 processPostParentChecker(layer, toUpload);
164 }
165 }
166
167 protected void processPostParentChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
168 APIDataSet ds = new APIDataSet(toUpload);
169 UploadAction action = new UploadAction();
170 action.uploadData(layer, ds);
171 }
172
173 /**
174 * Computes the collection of primitives to upload, given a collection of candidate
175 * primitives.
176 * Some of the candidates are excluded, i.e. if they aren't modified.
177 * Other primitives are added. A typical case is a primitive which is new and and
178 * which is referred by a modified relation. In order to upload the relation the
179 * new primitive has to be uploaded as well, even if it isn't included in the
180 * list of candidate primitives.
181 *
182 */
183 static class UploadHullBuilder implements Visitor {
184 private Set<OsmPrimitive> hull;
185
186 public UploadHullBuilder(){
187 hull = new HashSet<OsmPrimitive>();
188 }
189
190 public void visit(Node n) {
191 if (n.isNewOrUndeleted() || n.isModified() || n.isDeleted()) {
192 // upload new nodes as well as modified and deleted ones
193 hull.add(n);
194 }
195 }
196
197 public void visit(Way w) {
198 if (w.isNewOrUndeleted() || w.isModified() || w.isDeleted()) {
199 // upload new ways as well as modified and deleted ones
200 hull.add(w);
201 for (Node n: w.getNodes()) {
202 // we upload modified nodes even if they aren't in the current
203 // selection.
204 n.visit(this);
205 }
206 }
207 }
208
209 public void visit(Relation r) {
210 if (r.isNewOrUndeleted() || r.isModified() || r.isDeleted()) {
211 hull.add(r);
212 for (OsmPrimitive p : r.getMemberPrimitives()) {
213 // add new relation members. Don't include modified
214 // relation members. r shouldn't refer to deleted primitives,
215 // so wont check here for deleted primitives here
216 //
217 if (p.isNewOrUndeleted()) {
218 p.visit(this);
219 }
220 }
221 }
222 }
223
224 public void visit(Changeset cs) {
225 // do nothing
226 }
227
228 /**
229 * Builds the "hull" of primitives to be uploaded given a base collection
230 * of osm primitives.
231 *
232 * @param base the base collection. Must not be null.
233 * @return the "hull"
234 * @throws IllegalArgumentException thrown if base is null
235 */
236 public Set<OsmPrimitive> build(Collection<OsmPrimitive> base) throws IllegalArgumentException{
237 CheckParameterUtil.ensureParameterNotNull(base, "base");
238 hull = new HashSet<OsmPrimitive>();
239 for (OsmPrimitive p: base) {
240 p.visit(this);
241 }
242 return hull;
243 }
244 }
245
246 class DeletedParentsChecker extends PleaseWaitRunnable {
247 private boolean canceled;
248 private Exception lastException;
249 private Collection<OsmPrimitive> toUpload;
250 private OsmDataLayer layer;
251 private OsmServerBackreferenceReader reader;
252
253 /**
254 *
255 * @param layer the data layer for which a collection of selected primitives is uploaded
256 * @param toUpload the collection of primitives to upload
257 */
258 public DeletedParentsChecker(OsmDataLayer layer, Collection<OsmPrimitive> toUpload) {
259 super(tr("Checking parents for deleted objects"));
260 this.toUpload = toUpload;
261 this.layer = layer;
262 }
263
264 @Override
265 protected void cancel() {
266 this.canceled = true;
267 synchronized (this) {
268 if (reader != null) {
269 reader.cancel();
270 }
271 }
272 }
273
274 @Override
275 protected void finish() {
276 if (canceled)
277 return;
278 if (lastException != null) {
279 ExceptionUtil.explainException(lastException);
280 return;
281 }
282 Runnable r = new Runnable() {
283 public void run() {
284 processPostParentChecker(layer, toUpload);
285 }
286 };
287 SwingUtilities.invokeLater(r);
288 }
289
290 /**
291 * Replies the collection of deleted OSM primitives for which we have to check whether
292 * there are dangling references on the server.
293 *
294 * @return
295 */
296 protected Set<OsmPrimitive> getPrimitivesToCheckForParents() {
297 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
298 for (OsmPrimitive p: toUpload) {
299 if (p.isDeleted() && !p.isNewOrUndeleted()) {
300 ret.add(p);
301 }
302 }
303 return ret;
304 }
305
306 @Override
307 protected void realRun() throws SAXException, IOException, OsmTransferException {
308 try {
309 Stack<OsmPrimitive> toCheck = new Stack<OsmPrimitive>();
310 toCheck.addAll(getPrimitivesToCheckForParents());
311 Set<OsmPrimitive> checked = new HashSet<OsmPrimitive>();
312 while(!toCheck.isEmpty()) {
313 if (canceled) return;
314 OsmPrimitive current = toCheck.pop();
315 synchronized(this) {
316 reader = new OsmServerBackreferenceReader(current);
317 }
318 getProgressMonitor().subTask(tr("Reading parents of ''{0}''", current.getDisplayName(DefaultNameFormatter.getInstance())));
319 DataSet ds = reader.parseOsm(getProgressMonitor().createSubTaskMonitor(1, false));
320 synchronized(this) {
321 reader = null;
322 }
323 checked.add(current);
324 getProgressMonitor().subTask(tr("Checking for deleted parents in the local dataset"));
325 for (OsmPrimitive p: ds.allPrimitives()) {
326 if (canceled) return;
327 OsmPrimitive myDeletedParent = layer.data.getPrimitiveById(p);
328 // our local dataset includes a deleted parent of a primitive we want
329 // to delete. Include this parent in the collection of uploaded primitives
330 //
331 if (myDeletedParent != null && myDeletedParent.isDeleted()) {
332 if (!toUpload.contains(myDeletedParent)) {
333 toUpload.add(myDeletedParent);
334 }
335 if (!checked.contains(myDeletedParent)) {
336 toCheck.push(myDeletedParent);
337 }
338 }
339 }
340 }
341 } catch(Exception e) {
342 if (canceled)
343 // ignore exception
344 return;
345 lastException = e;
346 }
347 }
348 }
349 }