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