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 *   MainApplication.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.ensure(pid, "pid", "pid > 0", id -> id.getUniqueId() > 0);
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.getOsmId() &gt; 0 required.
116     * @return this task
117     * @throws IllegalArgumentException if the primitive is null
118     * @throws IllegalArgumentException if primitive.getOsmId() &lt;= 0
119     */
120    public HistoryLoadTask add(OsmPrimitive primitive) {
121        CheckParameterUtil.ensure(primitive, "primitive", "id > 0", prim -> prim.getOsmId() > 0);
122        return add(primitive.getOsmPrimitiveId());
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                add(primitive);
139            }
140        }
141        return this;
142    }
143
144    @Override
145    protected void cancel() {
146        if (reader != null) {
147            reader.cancel();
148        }
149        canceled = true;
150    }
151
152    @Override
153    protected void finish() {
154        if (isCanceled())
155            return;
156        if (lastException != null) {
157            ExceptionDialogUtil.explainException(lastException);
158            return;
159        }
160        HistoryDataSet.getInstance().mergeInto(loadedData);
161    }
162
163    @Override
164    protected void realRun() throws SAXException, IOException, OsmTransferException {
165        loadedData = new HistoryDataSet();
166        try {
167            progressMonitor.setTicksCount(toLoad.size());
168            for (PrimitiveId pid: toLoad) {
169                if (canceled) {
170                    break;
171                }
172                loadHistory(pid);
173            }
174        } catch (OsmTransferException e) {
175            lastException = e;
176        }
177    }
178
179    private void loadHistory(PrimitiveId pid) throws OsmTransferException {
180        String msg = getLoadingMessage(pid);
181        progressMonitor.indeterminateSubTask(tr(msg, Long.toString(pid.getUniqueId())));
182        reader = null;
183        HistoryDataSet ds;
184        try {
185            reader = new OsmServerHistoryReader(pid.getType(), pid.getUniqueId());
186            ds = loadHistory(reader, progressMonitor);
187        } catch (OsmTransferException e) {
188            if (canceled)
189                return;
190            throw e;
191        }
192        loadedData.mergeInto(ds);
193    }
194
195    protected static HistoryDataSet loadHistory(OsmServerHistoryReader reader, ProgressMonitor progressMonitor) throws OsmTransferException {
196        HistoryDataSet ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
197        if (ds != null) {
198            // load corresponding changesets (mostly for changeset comment)
199            OsmServerChangesetReader changesetReader = new OsmServerChangesetReader();
200            List<Long> changesetIds = new ArrayList<>(ds.getChangesetIds());
201
202            // query changesets 100 by 100 (OSM API limit)
203            int n = ChangesetQuery.MAX_CHANGESETS_NUMBER;
204            for (int i = 0; i < changesetIds.size(); i += n) {
205                for (Changeset c : changesetReader.queryChangesets(
206                        new ChangesetQuery().forChangesetIds(changesetIds.subList(i, Math.min(i + n, changesetIds.size()))),
207                        progressMonitor.createSubTaskMonitor(1, false))) {
208                    ds.putChangeset(c);
209                }
210            }
211        }
212        return ds;
213    }
214
215    protected static String getLoadingMessage(PrimitiveId pid) {
216        switch (pid.getType()) {
217        case NODE:
218            return marktr("Loading history for node {0}");
219        case WAY:
220            return marktr("Loading history for way {0}");
221        case RELATION:
222            return marktr("Loading history for relation {0}");
223        default:
224            return "";
225        }
226    }
227
228    /**
229     * Determines if this task has ben canceled.
230     * @return {@code true} if this task has ben canceled
231     */
232    public boolean isCanceled() {
233        return canceled;
234    }
235
236    /**
237     * Returns the last exception that occured during loading, if any.
238     * @return the last exception that occured during loading, or {@code null}
239     */
240    public Exception getLastException() {
241        return lastException;
242    }
243}