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    }