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