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