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