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 > 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() > 0 required. 116 * @return this task 117 * @throws IllegalArgumentException if the primitive is null 118 * @throws IllegalArgumentException if primitive.getOsmId() <= 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() > 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 <= 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}