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