001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.util.ArrayList;
009import java.util.Collection;
010import java.util.Iterator;
011import java.util.LinkedList;
012import java.util.List;
013
014import org.openstreetmap.josm.data.osm.Changeset;
015import org.openstreetmap.josm.data.osm.OsmPrimitive;
016import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
017import org.openstreetmap.josm.gui.io.UploadStrategySpecification;
018import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
019import org.openstreetmap.josm.gui.progress.ProgressMonitor;
020import org.openstreetmap.josm.tools.CheckParameterUtil;
021
022/**
023 * Class that uploads all changes to the osm server.
024 *
025 * This is done like this: - All objects with id = 0 are uploaded as new, except
026 * those in deleted, which are ignored - All objects in deleted list are
027 * deleted. - All remaining objects with modified flag set are updated.
028 */
029public class OsmServerWriter {
030    /**
031     * This list contains all successfully processed objects. The caller of
032     * upload* has to check this after the call and update its dataset.
033     *
034     * If a server connection error occurs, this may contain fewer entries
035     * than where passed in the list to upload*.
036     */
037    private Collection<OsmPrimitive> processed;
038
039    private static volatile List<OsmServerWritePostprocessor> postprocessors;
040    public static void registerPostprocessor(OsmServerWritePostprocessor pp) {
041        if (postprocessors == null) {
042            postprocessors = new ArrayList<>();
043        }
044        postprocessors.add(pp);
045    }
046
047    public static void unregisterPostprocessor(OsmServerWritePostprocessor pp) {
048        if (postprocessors != null) {
049            postprocessors.remove(pp);
050        }
051    }
052
053    private final OsmApi api = OsmApi.getOsmApi();
054    private boolean canceled;
055
056    private static final int MSECS_PER_SECOND = 1000;
057    private static final int SECONDS_PER_MINUTE = 60;
058    private static final int MSECS_PER_MINUTE = MSECS_PER_SECOND * SECONDS_PER_MINUTE;
059
060    private long uploadStartTime;
061
062    public String timeLeft(int progress, int list_size) {
063        long now = System.currentTimeMillis();
064        long elapsed = now - uploadStartTime;
065        if (elapsed == 0) {
066            elapsed = 1;
067        }
068        double uploads_per_ms = (double) progress / elapsed;
069        double uploads_left = list_size - progress;
070        long ms_left = (long) (uploads_left / uploads_per_ms);
071        long minutes_left = ms_left / MSECS_PER_MINUTE;
072        long seconds_left = (ms_left / MSECS_PER_SECOND) % SECONDS_PER_MINUTE;
073        StringBuilder time_left_str = new StringBuilder().append(minutes_left).append(':');
074        if (seconds_left < 10) {
075            time_left_str.append('0');
076        }
077        return time_left_str.append(seconds_left).toString();
078    }
079
080    /**
081     * Uploads the changes individually. Invokes one API call per uploaded primitmive.
082     *
083     * @param primitives the collection of primitives to upload
084     * @param progressMonitor the progress monitor
085     * @throws OsmTransferException if an exception occurs
086     */
087    protected void uploadChangesIndividually(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor)
088            throws OsmTransferException {
089        try {
090            progressMonitor.beginTask(tr("Starting to upload with one request per primitive ..."));
091            progressMonitor.setTicksCount(primitives.size());
092            uploadStartTime = System.currentTimeMillis();
093            for (OsmPrimitive osm : primitives) {
094                int progress = progressMonitor.getTicks();
095                String time_left_str = timeLeft(progress, primitives.size());
096                String msg = "";
097                switch(OsmPrimitiveType.from(osm)) {
098                case NODE: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading node ''{4}'' (id: {5})"); break;
099                case WAY: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading way ''{4}'' (id: {5})"); break;
100                case RELATION: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading relation ''{4}'' (id: {5})"); break;
101                }
102                progressMonitor.subTask(
103                        tr(msg,
104                                Math.round(100.0*progress/primitives.size()),
105                                progress,
106                                primitives.size(),
107                                time_left_str,
108                                osm.getName() == null ? osm.getId() : osm.getName(),
109                                        osm.getId()));
110                makeApiRequest(osm, progressMonitor);
111                processed.add(osm);
112                progressMonitor.worked(1);
113            }
114        } catch (OsmTransferException e) {
115            throw e;
116        } catch (Exception e) {
117            throw new OsmTransferException(e);
118        } finally {
119            progressMonitor.finishTask();
120        }
121    }
122
123    /**
124     * Upload all changes in one diff upload
125     *
126     * @param primitives the collection of primitives to upload
127     * @param progressMonitor  the progress monitor
128     * @throws OsmTransferException if an exception occurs
129     */
130    protected void uploadChangesAsDiffUpload(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor)
131            throws OsmTransferException {
132        try {
133            progressMonitor.beginTask(tr("Starting to upload in one request ..."));
134            processed.addAll(api.uploadDiff(primitives, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)));
135        } catch (OsmTransferException e) {
136            throw e;
137        } finally {
138            progressMonitor.finishTask();
139        }
140    }
141
142    /**
143     * Upload all changes in one diff upload
144     *
145     * @param primitives the collection of primitives to upload
146     * @param progressMonitor  the progress monitor
147     * @param chunkSize the size of the individual upload chunks. &gt; 0 required.
148     * @throws IllegalArgumentException if chunkSize &lt;= 0
149     * @throws OsmTransferException if an exception occurs
150     */
151    protected void uploadChangesInChunks(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor, int chunkSize)
152            throws OsmTransferException, IllegalArgumentException {
153        if (chunkSize <= 0)
154            throw new IllegalArgumentException(tr("Value >0 expected for parameter ''{0}'', got {1}", "chunkSize", chunkSize));
155        try {
156            progressMonitor.beginTask(tr("Starting to upload in chunks..."));
157            List<OsmPrimitive> chunk = new ArrayList<>(chunkSize);
158            Iterator<? extends OsmPrimitive> it = primitives.iterator();
159            int numChunks = (int) Math.ceil((double) primitives.size() / (double) chunkSize);
160            int i = 0;
161            while (it.hasNext()) {
162                i++;
163                if (canceled) return;
164                int j = 0;
165                chunk.clear();
166                while (it.hasNext() && j < chunkSize) {
167                    if (canceled) return;
168                    j++;
169                    chunk.add(it.next());
170                }
171                progressMonitor.setCustomText(
172                        trn("({0}/{1}) Uploading {2} object...",
173                                "({0}/{1}) Uploading {2} objects...",
174                                chunk.size(), i, numChunks, chunk.size()));
175                processed.addAll(api.uploadDiff(chunk, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)));
176            }
177        } catch (OsmTransferException e) {
178            throw e;
179        } finally {
180            progressMonitor.finishTask();
181        }
182    }
183
184    /**
185     * Send the dataset to the server.
186     *
187     * @param strategy the upload strategy. Must not be null.
188     * @param primitives list of objects to send
189     * @param changeset the changeset the data is uploaded to. Must not be null.
190     * @param monitor the progress monitor. If null, assumes {@link NullProgressMonitor#INSTANCE}
191     * @throws IllegalArgumentException if changeset is null
192     * @throws IllegalArgumentException if strategy is null
193     * @throws OsmTransferException if something goes wrong
194     */
195    public void uploadOsm(UploadStrategySpecification strategy, Collection<? extends OsmPrimitive> primitives,
196            Changeset changeset, ProgressMonitor monitor) throws OsmTransferException {
197        CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
198        processed = new LinkedList<>();
199        monitor = monitor == null ? NullProgressMonitor.INSTANCE : monitor;
200        monitor.beginTask(tr("Uploading data ..."));
201        try {
202            api.initialize(monitor);
203            // check whether we can use diff upload
204            if (changeset.getId() == 0) {
205                api.openChangeset(changeset, monitor.createSubTaskMonitor(0, false));
206            } else {
207                api.updateChangeset(changeset, monitor.createSubTaskMonitor(0, false));
208            }
209            api.setChangeset(changeset);
210            switch(strategy.getStrategy()) {
211            case SINGLE_REQUEST_STRATEGY:
212                uploadChangesAsDiffUpload(primitives, monitor.createSubTaskMonitor(0, false));
213                break;
214            case INDIVIDUAL_OBJECTS_STRATEGY:
215                uploadChangesIndividually(primitives, monitor.createSubTaskMonitor(0, false));
216                break;
217            case CHUNKED_DATASET_STRATEGY:
218                uploadChangesInChunks(primitives, monitor.createSubTaskMonitor(0, false), strategy.getChunkSize());
219                break;
220            }
221        } catch (OsmTransferException e) {
222            throw e;
223        } finally {
224            executePostprocessors(monitor);
225            monitor.finishTask();
226            api.setChangeset(null);
227        }
228    }
229
230    void makeApiRequest(OsmPrimitive osm, ProgressMonitor progressMonitor) throws OsmTransferException {
231        if (osm.isDeleted()) {
232            api.deletePrimitive(osm, progressMonitor);
233        } else if (osm.isNew()) {
234            api.createPrimitive(osm, progressMonitor);
235        } else {
236            api.modifyPrimitive(osm, progressMonitor);
237        }
238    }
239
240    public void cancel() {
241        this.canceled = true;
242        if (api != null) {
243            api.cancel();
244        }
245    }
246
247    /**
248     * Replies the collection of successfully processed primitives
249     *
250     * @return the collection of successfully processed primitives
251     */
252    public Collection<OsmPrimitive> getProcessedPrimitives() {
253        return processed;
254    }
255
256    /**
257     * Calls all registered upload postprocessors.
258     * @param pm progress monitor
259     */
260    public void executePostprocessors(ProgressMonitor pm) {
261        if (postprocessors != null) {
262            for (OsmServerWritePostprocessor pp : postprocessors) {
263                pp.postprocessUploadedPrimitives(processed, pm);
264            }
265        }
266    }
267}