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