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    private boolean getChangesetData = true;
056
057    /**
058     * Constructs a new {@code HistoryLoadTask}.
059     */
060    public HistoryLoadTask() {
061        super(tr("Load history"), true);
062    }
063
064    /**
065     * Constructs a new {@code HistoryLoadTask}.
066     *
067     * @param parent the component to be used as reference to find the
068     * parent for {@link org.openstreetmap.josm.gui.PleaseWaitDialog}.
069     * Must not be <code>null</code>.
070     * @throws IllegalArgumentException if parent is <code>null</code>
071     */
072    public HistoryLoadTask(Component parent) {
073        super(parent, tr("Load history"), true);
074        CheckParameterUtil.ensureParameterNotNull(parent, "parent");
075    }
076
077    /**
078     * Adds an object whose history is to be loaded.
079     *
080     * @param pid  the primitive id. Must not be null. Id &gt; 0 required.
081     * @return this task
082     */
083    public HistoryLoadTask add(PrimitiveId pid) {
084        CheckParameterUtil.ensure(pid, "pid", "pid > 0", id -> id.getUniqueId() > 0);
085        toLoad.add(pid);
086        return this;
087    }
088
089    /**
090     * Adds an object to be loaded, the object is specified by a history item.
091     *
092     * @param primitive the history item
093     * @return this task
094     * @throws IllegalArgumentException if primitive is null
095     */
096    public HistoryLoadTask add(HistoryOsmPrimitive primitive) {
097        CheckParameterUtil.ensureParameterNotNull(primitive, "primitive");
098        return add(primitive.getPrimitiveId());
099    }
100
101    /**
102     * Adds an object to be loaded, the object is specified by an already loaded object history.
103     *
104     * @param history the history. Must not be null.
105     * @return this task
106     * @throws IllegalArgumentException if history is null
107     */
108    public HistoryLoadTask add(History history) {
109        CheckParameterUtil.ensureParameterNotNull(history, "history");
110        return add(history.getPrimitiveId());
111    }
112
113    /**
114     * Adds an object to be loaded, the object is specified by an OSM primitive.
115     *
116     * @param primitive the OSM primitive. Must not be null. primitive.getOsmId() &gt; 0 required.
117     * @return this task
118     * @throws IllegalArgumentException if the primitive is null
119     * @throws IllegalArgumentException if primitive.getOsmId() &lt;= 0
120     */
121    public HistoryLoadTask add(OsmPrimitive primitive) {
122        CheckParameterUtil.ensure(primitive, "primitive", "id > 0", prim -> prim.getOsmId() > 0);
123        return add(primitive.getOsmPrimitiveId());
124    }
125
126    /**
127     * Adds a collection of objects to loaded, specified by a collection of OSM primitives.
128     *
129     * @param primitives the OSM primitives. Must not be <code>null</code>.
130     * <code>primitive.getId() &gt; 0</code> required.
131     * @return this task
132     * @throws IllegalArgumentException if primitives is <code>null</code>
133     * @throws IllegalArgumentException if one of the ids in the collection &lt;= 0
134     */
135    public HistoryLoadTask add(Collection<? extends OsmPrimitive> primitives) {
136        CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
137        for (OsmPrimitive primitive: primitives) {
138            if (primitive != null) {
139                add(primitive);
140            }
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        int ticks = toLoad.size();
168        if (getChangesetData)
169            ticks *= 2;
170        try {
171            progressMonitor.setTicksCount(ticks);
172            for (PrimitiveId pid: toLoad) {
173                if (canceled) {
174                    break;
175                }
176                loadHistory(pid);
177            }
178        } catch (OsmTransferException e) {
179            lastException = e;
180        }
181    }
182
183    private void loadHistory(PrimitiveId pid) throws OsmTransferException {
184        String msg = getLoadingMessage(pid);
185        progressMonitor.indeterminateSubTask(tr(msg, Long.toString(pid.getUniqueId())));
186        reader = null;
187        HistoryDataSet ds;
188        try {
189            reader = new OsmServerHistoryReader(pid.getType(), pid.getUniqueId());
190            if (getChangesetData) {
191                ds = loadHistory(reader, progressMonitor);
192            } else {
193                ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
194            }
195        } catch (OsmTransferException e) {
196            if (canceled)
197                return;
198            throw e;
199        }
200        loadedData.mergeInto(ds);
201    }
202
203    protected static HistoryDataSet loadHistory(OsmServerHistoryReader reader, ProgressMonitor progressMonitor) throws OsmTransferException {
204        HistoryDataSet ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
205        if (ds != null) {
206            // load corresponding changesets (mostly for changeset comment)
207            OsmServerChangesetReader changesetReader = new OsmServerChangesetReader();
208            List<Long> changesetIds = new ArrayList<>(ds.getChangesetIds());
209
210            // query changesets 100 by 100 (OSM API limit)
211            int n = ChangesetQuery.MAX_CHANGESETS_NUMBER;
212            for (int i = 0; i < changesetIds.size(); i += n) {
213                for (Changeset c : changesetReader.queryChangesets(
214                        new ChangesetQuery().forChangesetIds(changesetIds.subList(i, Math.min(i + n, changesetIds.size()))),
215                        progressMonitor.createSubTaskMonitor(1, false))) {
216                    ds.putChangeset(c);
217                }
218            }
219        }
220        return ds;
221    }
222
223    protected static String getLoadingMessage(PrimitiveId pid) {
224        switch (pid.getType()) {
225        case NODE:
226            return marktr("Loading history for node {0}");
227        case WAY:
228            return marktr("Loading history for way {0}");
229        case RELATION:
230            return marktr("Loading history for relation {0}");
231        default:
232            return "";
233        }
234    }
235
236    /**
237     * Determines if this task has ben canceled.
238     * @return {@code true} if this task has ben canceled
239     */
240    public boolean isCanceled() {
241        return canceled;
242    }
243
244    /**
245     * Returns the last exception that occurred during loading, if any.
246     * @return the last exception that occurred during loading, or {@code null}
247     */
248    public Exception getLastException() {
249        return lastException;
250    }
251
252    /**
253     * Determine if changeset information is needed. By default it is retrieved.
254     * @param b false means don't retrieve changeset data.
255     * @since 14763
256     */
257    public void setChangesetDataNeeded(boolean b) {
258        getChangesetData = b;
259    }
260}