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 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    public static void unregisterPostprocessor(OsmServerWritePostprocessor pp) {
047        if (postprocessors != null) {
048            postprocessors.remove(pp);
049        }
050    }
051
052    private OsmApi api = OsmApi.getOsmApi();
053    private boolean canceled = false;
054
055    private static final int MSECS_PER_SECOND = 1000;
056    private static final int SECONDS_PER_MINUTE = 60;
057    private static final int MSECS_PER_MINUTE = MSECS_PER_SECOND * SECONDS_PER_MINUTE;
058
059    long uploadStartTime;
060
061    public String timeLeft(int progress, int list_size) {
062        long now = System.currentTimeMillis();
063        long elapsed = now - uploadStartTime;
064        if (elapsed == 0) {
065            elapsed = 1;
066        }
067        float uploads_per_ms = (float)progress / elapsed;
068        float uploads_left = list_size - progress;
069        int ms_left = (int)(uploads_left / uploads_per_ms);
070        int minutes_left = ms_left / MSECS_PER_MINUTE;
071        int seconds_left = (ms_left / MSECS_PER_SECOND) % SECONDS_PER_MINUTE ;
072        String time_left_str = Integer.toString(minutes_left) + ":";
073        if (seconds_left < 10) {
074            time_left_str += "0";
075        }
076        time_left_str += Integer.toString(seconds_left);
077        return time_left_str;
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 thrown if an exception occurs
086     */
087    protected void uploadChangesIndividually(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor) throws OsmTransferException {
088        try {
089            progressMonitor.beginTask(tr("Starting to upload with one request per primitive ..."));
090            progressMonitor.setTicksCount(primitives.size());
091            uploadStartTime = System.currentTimeMillis();
092            for (OsmPrimitive osm : primitives) {
093                int progress = progressMonitor.getTicks();
094                String time_left_str = timeLeft(progress, primitives.size());
095                String msg = "";
096                switch(OsmPrimitiveType.from(osm)) {
097                case NODE: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading node ''{4}'' (id: {5})"); break;
098                case WAY: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading way ''{4}'' (id: {5})"); break;
099                case RELATION: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading relation ''{4}'' (id: {5})"); break;
100                }
101                progressMonitor.subTask(
102                        tr(msg,
103                                Math.round(100.0*progress/primitives.size()),
104                                progress,
105                                primitives.size(),
106                                time_left_str,
107                                osm.getName() == null ? osm.getId() : osm.getName(),
108                                        osm.getId()));
109                makeApiRequest(osm,progressMonitor);
110                processed.add(osm);
111                progressMonitor.worked(1);
112            }
113        } catch(OsmTransferException e) {
114            throw e;
115        } catch(Exception e) {
116            throw new OsmTransferException(e);
117        } finally {
118            progressMonitor.finishTask();
119        }
120    }
121
122    /**
123     * Upload all changes in one diff upload
124     *
125     * @param primitives the collection of primitives to upload
126     * @param progressMonitor  the progress monitor
127     * @throws OsmTransferException thrown if an exception occurs
128     */
129    protected void uploadChangesAsDiffUpload(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor) throws OsmTransferException {
130        try {
131            progressMonitor.beginTask(tr("Starting to upload in one request ..."));
132            processed.addAll(api.uploadDiff(primitives, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)));
133        } catch(OsmTransferException e) {
134            throw e;
135        } finally {
136            progressMonitor.finishTask();
137        }
138    }
139
140    /**
141     * Upload all changes in one diff upload
142     *
143     * @param primitives the collection of primitives to upload
144     * @param progressMonitor  the progress monitor
145     * @param chunkSize the size of the individual upload chunks. &gt; 0 required.
146     * @throws IllegalArgumentException thrown if chunkSize &lt;= 0
147     * @throws OsmTransferException thrown if an exception occurs
148     */
149    protected void uploadChangesInChunks(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor, int chunkSize) throws OsmTransferException, IllegalArgumentException {
150        if (chunkSize <=0)
151            throw new IllegalArgumentException(tr("Value >0 expected for parameter ''{0}'', got {1}", "chunkSize", chunkSize));
152        try {
153            progressMonitor.beginTask(tr("Starting to upload in chunks..."));
154            List<OsmPrimitive> chunk = new ArrayList<>(chunkSize);
155            Iterator<? extends OsmPrimitive> it = primitives.iterator();
156            int numChunks = (int)Math.ceil((double)primitives.size() / (double)chunkSize);
157            int i= 0;
158            while(it.hasNext()) {
159                i++;
160                if (canceled) return;
161                int j = 0;
162                chunk.clear();
163                while(it.hasNext() && j < chunkSize) {
164                    if (canceled) return;
165                    j++;
166                    chunk.add(it.next());
167                }
168                progressMonitor.setCustomText(
169                        trn("({0}/{1}) Uploading {2} object...",
170                                "({0}/{1}) Uploading {2} objects...",
171                                chunk.size(), i, numChunks, chunk.size()));
172                processed.addAll(api.uploadDiff(chunk, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)));
173            }
174        } catch(OsmTransferException e) {
175            throw e;
176        } finally {
177            progressMonitor.finishTask();
178        }
179    }
180
181    /**
182     * Send the dataset to the server.
183     *
184     * @param strategy the upload strategy. Must not be null.
185     * @param primitives list of objects to send
186     * @param changeset the changeset the data is uploaded to. Must not be null.
187     * @param monitor the progress monitor. If null, assumes {@link NullProgressMonitor#INSTANCE}
188     * @throws IllegalArgumentException thrown if changeset is null
189     * @throws IllegalArgumentException thrown if strategy is null
190     * @throws OsmTransferException thrown if something goes wrong
191     */
192    public void uploadOsm(UploadStrategySpecification strategy, Collection<? extends OsmPrimitive> primitives, Changeset changeset, ProgressMonitor monitor) throws OsmTransferException {
193        CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
194        processed = new LinkedList<>();
195        monitor = monitor == null ? NullProgressMonitor.INSTANCE : monitor;
196        monitor.beginTask(tr("Uploading data ..."));
197        try {
198            api.initialize(monitor);
199            // check whether we can use diff upload
200            if (changeset.getId() == 0) {
201                api.openChangeset(changeset,monitor.createSubTaskMonitor(0, false));
202            } else {
203                api.updateChangeset(changeset,monitor.createSubTaskMonitor(0, false));
204            }
205            api.setChangeset(changeset);
206            switch(strategy.getStrategy()) {
207            case SINGLE_REQUEST_STRATEGY:
208                uploadChangesAsDiffUpload(primitives,monitor.createSubTaskMonitor(0,false));
209                break;
210            case INDIVIDUAL_OBJECTS_STRATEGY:
211                uploadChangesIndividually(primitives,monitor.createSubTaskMonitor(0,false));
212                break;
213            case CHUNKED_DATASET_STRATEGY:
214                uploadChangesInChunks(primitives,monitor.createSubTaskMonitor(0,false), strategy.getChunkSize());
215                break;
216            }
217        } catch(OsmTransferException e) {
218            throw e;
219        } finally {
220            executePostprocessors(monitor);
221            monitor.finishTask();
222            api.setChangeset(null);
223        }
224    }
225
226    void makeApiRequest(OsmPrimitive osm, ProgressMonitor progressMonitor) throws OsmTransferException {
227        if (osm.isDeleted()) {
228            api.deletePrimitive(osm, progressMonitor);
229        } else if (osm.isNew()) {
230            api.createPrimitive(osm, progressMonitor);
231        } else {
232            api.modifyPrimitive(osm, progressMonitor);
233        }
234    }
235
236    public void cancel() {
237        this.canceled = true;
238        if (api != null) {
239            api.cancel();
240        }
241    }
242
243    /**
244     * Replies the collection of successfully processed primitives
245     *
246     * @return the collection of successfully processed primitives
247     */
248    public Collection<OsmPrimitive> getProcessedPrimitives() {
249        return processed;
250    }
251
252    /**
253     * Calls all registered upload postprocessors.
254     */
255    public void executePostprocessors(ProgressMonitor pm) {
256        if (postprocessors != null) {
257            for (OsmServerWritePostprocessor pp : postprocessors) {
258                pp.postprocessUploadedPrimitives(processed, pm);
259            }
260        }
261    }
262}