001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.downloadtasks; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.Arrays; 007import java.util.Date; 008import java.util.HashMap; 009import java.util.Iterator; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.Optional; 013import java.util.concurrent.Future; 014import java.util.concurrent.RejectedExecutionException; 015import java.util.regex.Matcher; 016 017import org.openstreetmap.josm.data.Bounds; 018import org.openstreetmap.josm.data.osm.DataSet; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.NodeData; 021import org.openstreetmap.josm.data.osm.OsmPrimitive; 022import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 023import org.openstreetmap.josm.data.osm.PrimitiveData; 024import org.openstreetmap.josm.data.osm.PrimitiveId; 025import org.openstreetmap.josm.data.osm.RelationData; 026import org.openstreetmap.josm.data.osm.WayData; 027import org.openstreetmap.josm.data.osm.history.History; 028import org.openstreetmap.josm.data.osm.history.HistoryDataSet; 029import org.openstreetmap.josm.data.osm.history.HistoryDataSetListener; 030import org.openstreetmap.josm.data.osm.history.HistoryNode; 031import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; 032import org.openstreetmap.josm.data.osm.history.HistoryRelation; 033import org.openstreetmap.josm.data.osm.history.HistoryWay; 034import org.openstreetmap.josm.gui.MainApplication; 035import org.openstreetmap.josm.gui.history.HistoryLoadTask; 036import org.openstreetmap.josm.gui.progress.ProgressMonitor; 037import org.openstreetmap.josm.io.Compression; 038import org.openstreetmap.josm.io.OsmApi; 039import org.openstreetmap.josm.io.OsmServerLocationReader; 040import org.openstreetmap.josm.io.OsmServerReader; 041import org.openstreetmap.josm.io.OsmTransferException; 042import org.openstreetmap.josm.io.UrlPatterns.OsmChangeUrlPattern; 043import org.openstreetmap.josm.tools.Logging; 044 045/** 046 * Task allowing to download OsmChange data (http://wiki.openstreetmap.org/wiki/OsmChange). 047 * @since 4530 048 */ 049public class DownloadOsmChangeTask extends DownloadOsmTask { 050 051 @Override 052 public String[] getPatterns() { 053 return patterns(OsmChangeUrlPattern.class); 054 } 055 056 @Override 057 public String getTitle() { 058 return tr("Download OSM Change"); 059 } 060 061 @Override 062 public Future<?> download(DownloadParams settings, Bounds downloadArea, ProgressMonitor progressMonitor) { 063 return null; 064 } 065 066 @Override 067 public Future<?> loadUrl(DownloadParams settings, final String url, ProgressMonitor progressMonitor) { 068 Optional<OsmChangeUrlPattern> urlPattern = Arrays.stream(OsmChangeUrlPattern.values()).filter(p -> p.matches(url)).findFirst(); 069 String newUrl = url; 070 final Matcher matcher = OsmChangeUrlPattern.OSM_WEBSITE.matcher(url); 071 if (matcher.matches()) { 072 newUrl = OsmApi.getOsmApi().getBaseUrl() + "changeset/" + Long.parseLong(matcher.group(2)) + "/download"; 073 } 074 downloadTask = new DownloadTask(settings, new OsmServerLocationReader(newUrl), progressMonitor, true, 075 Compression.byExtension(newUrl)); 076 // Extract .osc filename from URL to set the new layer name 077 extractOsmFilename(settings, urlPattern.orElse(OsmChangeUrlPattern.EXTERNAL_OSC_FILE).pattern(), newUrl); 078 return MainApplication.worker.submit(downloadTask); 079 } 080 081 /** 082 * OsmChange download task. 083 */ 084 protected class DownloadTask extends DownloadOsmTask.DownloadTask { 085 086 /** 087 * Constructs a new {@code DownloadTask}. 088 * @param settings download settings 089 * @param reader OSM data reader 090 * @param progressMonitor progress monitor 091 * @param zoomAfterDownload If true, the map view will zoom to download area after download 092 * @param compression compression to use 093 */ 094 public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor, 095 boolean zoomAfterDownload, Compression compression) { 096 super(settings, reader, progressMonitor, zoomAfterDownload, compression); 097 } 098 099 @Override 100 protected DataSet parseDataSet() throws OsmTransferException { 101 return reader.parseOsmChange(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false), 102 compression); 103 } 104 105 @Override 106 protected void finish() { 107 super.finish(); 108 if (isFailed() || isCanceled() || downloadedData == null) 109 return; // user canceled download or error occurred 110 try { 111 // A changeset does not contain all referred primitives, this is the map of incomplete ones 112 // For each incomplete primitive, we'll have to get its state at date it was referred 113 Map<OsmPrimitive, Date> toLoad = new HashMap<>(); 114 for (OsmPrimitive p : downloadedData.allNonDeletedPrimitives()) { 115 if (p.isIncomplete()) { 116 Date timestamp = null; 117 for (OsmPrimitive ref : p.getReferrers()) { 118 if (!ref.isTimestampEmpty()) { 119 timestamp = ref.getTimestamp(); 120 break; 121 } 122 } 123 toLoad.put(p, timestamp); 124 } 125 } 126 if (isCanceled()) return; 127 // Let's load all required history 128 MainApplication.worker.submit(new HistoryLoaderAndListener(toLoad)); 129 } catch (RejectedExecutionException e) { 130 rememberException(e); 131 setFailed(true); 132 } 133 } 134 } 135 136 /** 137 * Loads history and updates incomplete primitives. 138 */ 139 private static final class HistoryLoaderAndListener extends HistoryLoadTask implements HistoryDataSetListener { 140 141 private final Map<OsmPrimitive, Date> toLoad; 142 143 private HistoryLoaderAndListener(Map<OsmPrimitive, Date> toLoad) { 144 this.toLoad = toLoad; 145 this.setChangesetDataNeeded(false); 146 add(toLoad.keySet()); 147 // Updating process is done after all history requests have been made 148 HistoryDataSet.getInstance().addHistoryDataSetListener(this); 149 } 150 151 @Override 152 public void historyUpdated(HistoryDataSet source, PrimitiveId id) { 153 Map<OsmPrimitive, Date> toLoadNext = new HashMap<>(); 154 for (Iterator<Entry<OsmPrimitive, Date>> it = toLoad.entrySet().iterator(); it.hasNext();) { 155 Entry<OsmPrimitive, Date> entry = it.next(); 156 OsmPrimitive p = entry.getKey(); 157 History history = source.getHistory(p.getPrimitiveId()); 158 Date date = entry.getValue(); 159 // If the history has been loaded and a timestamp is known 160 if (history != null && date != null) { 161 // Lookup for the primitive version at the specified timestamp 162 HistoryOsmPrimitive hp = history.getByDate(date); 163 if (hp != null) { 164 PrimitiveData data; 165 166 switch (p.getType()) { 167 case NODE: 168 data = ((HistoryNode) hp).fillPrimitiveData(new NodeData()); 169 break; 170 case WAY: 171 data = ((HistoryWay) hp).fillPrimitiveData(new WayData()); 172 // Find incomplete nodes to load at next run 173 for (Long nodeId : ((HistoryWay) hp).getNodes()) { 174 if (p.getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE) == null) { 175 Node n = new Node(nodeId); 176 p.getDataSet().addPrimitive(n); 177 toLoadNext.put(n, date); 178 } 179 } 180 break; 181 case RELATION: 182 data = ((HistoryRelation) hp).fillPrimitiveData(new RelationData()); 183 break; 184 default: throw new AssertionError("Unknown primitive type"); 185 } 186 187 // Load the history data 188 try { 189 p.load(data); 190 // Forget this primitive 191 it.remove(); 192 } catch (AssertionError e) { 193 Logging.log(Logging.LEVEL_ERROR, "Cannot load "+p+':', e); 194 } 195 } 196 } 197 } 198 source.removeHistoryDataSetListener(this); 199 if (toLoadNext.isEmpty()) { 200 // No more primitive to update. Processing is finished 201 // Be sure all updated primitives are correctly drawn 202 MainApplication.getMap().repaint(); 203 } else { 204 // Some primitives still need to be loaded 205 // Let's load all required history 206 MainApplication.worker.submit(new HistoryLoaderAndListener(toLoadNext)); 207 } 208 } 209 210 @Override 211 public void historyDataSetCleared(HistoryDataSet source) { 212 // Do nothing 213 } 214 } 215}