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