001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.io.IOException;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.List;
013import java.util.Set;
014
015import org.openstreetmap.josm.data.osm.Changeset;
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.osm.PrimitiveId;
018import org.openstreetmap.josm.data.osm.history.History;
019import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
020import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
021import org.openstreetmap.josm.gui.ExceptionDialogUtil;
022import org.openstreetmap.josm.gui.PleaseWaitRunnable;
023import org.openstreetmap.josm.gui.progress.ProgressMonitor;
024import org.openstreetmap.josm.io.ChangesetQuery;
025import org.openstreetmap.josm.io.OsmServerChangesetReader;
026import org.openstreetmap.josm.io.OsmServerHistoryReader;
027import org.openstreetmap.josm.io.OsmTransferException;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.xml.sax.SAXException;
030
031/**
032 * Loads the object history of a collection of objects from the server.
033 *
034 * It provides a fluent API for configuration.
035 *
036 * Sample usage:
037 *
038 * <pre>
039 *   HistoryLoadTask task = new HistoryLoadTask()
040 *      .add(node)
041 *      .add(way)
042 *      .add(relation)
043 *      .add(aHistoryItem);
044 *
045 *   Main.worker.execute(task);
046 * </pre>
047 */
048public class HistoryLoadTask extends PleaseWaitRunnable {
049
050    private boolean canceled;
051    private Exception lastException;
052    private final Set<PrimitiveId> toLoad = new HashSet<>();
053    private HistoryDataSet loadedData;
054    private OsmServerHistoryReader reader;
055
056    /**
057     * Constructs a new {@code HistoryLoadTask}.
058     */
059    public HistoryLoadTask() {
060        super(tr("Load history"), true);
061    }
062
063    /**
064     * Constructs a new {@code HistoryLoadTask}.
065     *
066     * @param parent the component to be used as reference to find the
067     * parent for {@link org.openstreetmap.josm.gui.PleaseWaitDialog}.
068     * Must not be <code>null</code>.
069     * @throws IllegalArgumentException if parent is <code>null</code>
070     */
071    public HistoryLoadTask(Component parent) {
072        super(parent, tr("Load history"), true);
073        CheckParameterUtil.ensureParameterNotNull(parent, "parent");
074    }
075
076    /**
077     * Adds an object whose history is to be loaded.
078     *
079     * @param pid  the primitive id. Must not be null. Id &gt; 0 required.
080     * @return this task
081     */
082    public HistoryLoadTask add(PrimitiveId pid) {
083        CheckParameterUtil.ensureValidPrimitiveId(pid, "pid");
084        toLoad.add(pid);
085        return this;
086    }
087
088    /**
089     * Adds an object to be loaded, the object is specified by a history item.
090     *
091     * @param primitive the history item
092     * @return this task
093     * @throws IllegalArgumentException if primitive is null
094     */
095    public HistoryLoadTask add(HistoryOsmPrimitive primitive) {
096        CheckParameterUtil.ensureParameterNotNull(primitive, "primitive");
097        return add(primitive.getPrimitiveId());
098    }
099
100    /**
101     * Adds an object to be loaded, the object is specified by an already loaded object history.
102     *
103     * @param history the history. Must not be null.
104     * @return this task
105     * @throws IllegalArgumentException if history is null
106     */
107    public HistoryLoadTask add(History history) {
108        CheckParameterUtil.ensureParameterNotNull(history, "history");
109        return add(history.getPrimitiveId());
110    }
111
112    /**
113     * Adds an object to be loaded, the object is specified by an OSM primitive.
114     *
115     * @param primitive the OSM primitive. Must not be null. primitive.getId() &gt; 0 required.
116     * @return this task
117     * @throws IllegalArgumentException if the primitive is null
118     * @throws IllegalArgumentException if primitive.getId() &lt;= 0
119     */
120    public HistoryLoadTask add(OsmPrimitive primitive) {
121        CheckParameterUtil.ensureValidPrimitiveId(primitive, "primitive");
122        return add(primitive.getPrimitiveId());
123    }
124
125    /**
126     * Adds a collection of objects to loaded, specified by a collection of OSM primitives.
127     *
128     * @param primitives the OSM primitives. Must not be <code>null</code>.
129     * <code>primitive.getId() &gt; 0</code> required.
130     * @return this task
131     * @throws IllegalArgumentException if primitives is <code>null</code>
132     * @throws IllegalArgumentException if one of the ids in the collection &lt;= 0
133     */
134    public HistoryLoadTask add(Collection<? extends OsmPrimitive> primitives) {
135        CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
136        for (OsmPrimitive primitive: primitives) {
137            if (primitive == null) {
138                continue;
139            }
140            add(primitive);
141        }
142        return this;
143    }
144
145    @Override
146    protected void cancel() {
147        if (reader != null) {
148            reader.cancel();
149        }
150        canceled = true;
151    }
152
153    @Override
154    protected void finish() {
155        if (isCanceled())
156            return;
157        if (lastException != null) {
158            ExceptionDialogUtil.explainException(lastException);
159            return;
160        }
161        HistoryDataSet.getInstance().mergeInto(loadedData);
162    }
163
164    @Override
165    protected void realRun() throws SAXException, IOException, OsmTransferException {
166        loadedData = new HistoryDataSet();
167        try {
168            progressMonitor.setTicksCount(toLoad.size());
169            for (PrimitiveId pid: toLoad) {
170                if (canceled) {
171                    break;
172                }
173                String msg = getLoadingMessage(pid);
174                progressMonitor.indeterminateSubTask(tr(msg, Long.toString(pid.getUniqueId())));
175                reader = null;
176                HistoryDataSet ds;
177                try {
178                    reader = new OsmServerHistoryReader(pid.getType(), pid.getUniqueId());
179                    ds = loadHistory(reader, progressMonitor);
180                } catch (OsmTransferException e) {
181                    if (canceled)
182                        return;
183                    throw e;
184                }
185                loadedData.mergeInto(ds);
186            }
187        } catch (OsmTransferException e) {
188            lastException = e;
189            return;
190        }
191    }
192
193    protected static HistoryDataSet loadHistory(OsmServerHistoryReader reader, ProgressMonitor progressMonitor) throws OsmTransferException {
194        HistoryDataSet ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
195        if (ds != null) {
196            // load corresponding changesets (mostly for changeset comment)
197            OsmServerChangesetReader changesetReader = new OsmServerChangesetReader();
198            List<Long> changesetIds = new ArrayList<>(ds.getChangesetIds());
199
200            // query changesets 100 by 100 (OSM API limit)
201            int n = ChangesetQuery.MAX_CHANGESETS_NUMBER;
202            for (int i = 0; i < changesetIds.size(); i += n) {
203                for (Changeset c : changesetReader.queryChangesets(
204                        new ChangesetQuery().forChangesetIds(changesetIds.subList(i, Math.min(i + n, changesetIds.size()))),
205                        progressMonitor.createSubTaskMonitor(1, false))) {
206                    ds.putChangeset(c);
207                }
208            }
209        }
210        return ds;
211    }
212
213    protected static String getLoadingMessage(PrimitiveId pid) {
214        switch (pid.getType()) {
215        case NODE:
216            return marktr("Loading history for node {0}");
217        case WAY:
218            return marktr("Loading history for way {0}");
219        case RELATION:
220            return marktr("Loading history for relation {0}");
221        default:
222            return "";
223        }
224    }
225
226    /**
227     * Determines if this task has ben canceled.
228     * @return {@code true} if this task has ben canceled
229     */
230    public boolean isCanceled() {
231        return canceled;
232    }
233
234    /**
235     * Returns the last exception that occured during loading, if any.
236     * @return the last exception that occured during loading, or {@code null}
237     */
238    public Exception getLastException() {
239        return lastException;
240    }
241}