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.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.ensure(primitive, "primitive", "id > 0", prim -> prim.getUniqueId() > 0);
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                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            return;
177        }
178    }
179
180    private void loadHistory(PrimitiveId pid) throws OsmTransferException {
181        String msg = getLoadingMessage(pid);
182        progressMonitor.indeterminateSubTask(tr(msg, Long.toString(pid.getUniqueId())));
183        reader = null;
184        HistoryDataSet ds;
185        try {
186            reader = new OsmServerHistoryReader(pid.getType(), pid.getUniqueId());
187            ds = loadHistory(reader, progressMonitor);
188        } catch (OsmTransferException e) {
189            if (canceled)
190                return;
191            throw e;
192        }
193        loadedData.mergeInto(ds);
194    }
195
196    protected static HistoryDataSet loadHistory(OsmServerHistoryReader reader, ProgressMonitor progressMonitor) throws OsmTransferException {
197        HistoryDataSet ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
198        if (ds != null) {
199            // load corresponding changesets (mostly for changeset comment)
200            OsmServerChangesetReader changesetReader = new OsmServerChangesetReader();
201            List<Long> changesetIds = new ArrayList<>(ds.getChangesetIds());
202
203            // query changesets 100 by 100 (OSM API limit)
204            int n = ChangesetQuery.MAX_CHANGESETS_NUMBER;
205            for (int i = 0; i < changesetIds.size(); i += n) {
206                for (Changeset c : changesetReader.queryChangesets(
207                        new ChangesetQuery().forChangesetIds(changesetIds.subList(i, Math.min(i + n, changesetIds.size()))),
208                        progressMonitor.createSubTaskMonitor(1, false))) {
209                    ds.putChangeset(c);
210                }
211            }
212        }
213        return ds;
214    }
215
216    protected static String getLoadingMessage(PrimitiveId pid) {
217        switch (pid.getType()) {
218        case NODE:
219            return marktr("Loading history for node {0}");
220        case WAY:
221            return marktr("Loading history for way {0}");
222        case RELATION:
223            return marktr("Loading history for relation {0}");
224        default:
225            return "";
226        }
227    }
228
229    /**
230     * Determines if this task has ben canceled.
231     * @return {@code true} if this task has ben canceled
232     */
233    public boolean isCanceled() {
234        return canceled;
235    }
236
237    /**
238     * Returns the last exception that occured during loading, if any.
239     * @return the last exception that occured during loading, or {@code null}
240     */
241    public Exception getLastException() {
242        return lastException;
243    }
244}