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.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.concurrent.Future; 013import java.util.regex.Matcher; 014import java.util.regex.Pattern; 015 016import org.openstreetmap.josm.Main; 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.RelationMemberData; 027import org.openstreetmap.josm.data.osm.WayData; 028import org.openstreetmap.josm.data.osm.history.History; 029import org.openstreetmap.josm.data.osm.history.HistoryDataSet; 030import org.openstreetmap.josm.data.osm.history.HistoryDataSetListener; 031import org.openstreetmap.josm.data.osm.history.HistoryNode; 032import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; 033import org.openstreetmap.josm.data.osm.history.HistoryRelation; 034import org.openstreetmap.josm.data.osm.history.HistoryWay; 035import org.openstreetmap.josm.gui.history.HistoryLoadTask; 036import org.openstreetmap.josm.gui.progress.ProgressMonitor; 037import org.openstreetmap.josm.io.OsmApi; 038import org.openstreetmap.josm.io.OsmServerLocationReader; 039import org.openstreetmap.josm.io.OsmServerReader; 040import org.openstreetmap.josm.io.OsmTransferException; 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 Main.worker.submit(downloadTask); 079 } 080 081 protected class DownloadTask extends DownloadOsmTask.DownloadTask { 082 083 public DownloadTask(boolean newLayer, OsmServerReader reader, 084 ProgressMonitor progressMonitor) { 085 super(newLayer, reader, progressMonitor); 086 } 087 088 @Override 089 protected DataSet parseDataSet() throws OsmTransferException { 090 return reader.parseOsmChange(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 091 } 092 093 @Override 094 protected void finish() { 095 super.finish(); 096 if (isFailed() || isCanceled() || downloadedData == null) 097 return; // user canceled download or error occurred 098 try { 099 // A changeset does not contain all referred primitives, this is the map of incomplete ones 100 // For each incomplete primitive, we'll have to get its state at date it was referred 101 Map<OsmPrimitive, Date> toLoad = new HashMap<>(); 102 for (OsmPrimitive p : downloadedData.allNonDeletedPrimitives()) { 103 if (p.isIncomplete()) { 104 Date timestamp = null; 105 for (OsmPrimitive ref : p.getReferrers()) { 106 if (!ref.isTimestampEmpty()) { 107 timestamp = ref.getTimestamp(); 108 break; 109 } 110 } 111 toLoad.put(p, timestamp); 112 } 113 } 114 if (isCanceled()) return; 115 // Let's load all required history 116 Main.worker.submit(new HistoryLoaderAndListener(toLoad)); 117 } catch (Exception e) { 118 rememberException(e); 119 setFailed(true); 120 } 121 } 122 } 123 124 /** 125 * Loads history and updates incomplete primitives. 126 */ 127 private static final class HistoryLoaderAndListener extends HistoryLoadTask implements HistoryDataSetListener { 128 129 private final Map<OsmPrimitive, Date> toLoad; 130 131 private HistoryLoaderAndListener(Map<OsmPrimitive, Date> toLoad) { 132 this.toLoad = toLoad; 133 add(toLoad.keySet()); 134 // Updating process is done after all history requests have been made 135 HistoryDataSet.getInstance().addHistoryDataSetListener(this); 136 } 137 138 @Override 139 public void historyUpdated(HistoryDataSet source, PrimitiveId id) { 140 Map<OsmPrimitive, Date> toLoadNext = new HashMap<>(); 141 for (Iterator<Entry<OsmPrimitive, Date>> it = toLoad.entrySet().iterator(); it.hasNext();) { 142 Entry<OsmPrimitive, Date> entry = it.next(); 143 OsmPrimitive p = entry.getKey(); 144 History history = source.getHistory(p.getPrimitiveId()); 145 Date date = entry.getValue(); 146 // If the history has been loaded and a timestamp is known 147 if (history != null && date != null) { 148 // Lookup for the primitive version at the specified timestamp 149 HistoryOsmPrimitive hp = history.getByDate(date); 150 if (hp != null) { 151 PrimitiveData data; 152 153 switch (p.getType()) { 154 case NODE: 155 data = new NodeData(); 156 ((NodeData) data).setCoor(((HistoryNode) hp).getCoords()); 157 break; 158 case WAY: 159 data = new WayData(); 160 List<Long> nodeIds = ((HistoryWay) hp).getNodes(); 161 ((WayData) data).setNodes(nodeIds); 162 // Find incomplete nodes to load at next run 163 for (Long nodeId : nodeIds) { 164 if (p.getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE) == null) { 165 Node n = new Node(nodeId); 166 p.getDataSet().addPrimitive(n); 167 toLoadNext.put(n, date); 168 } 169 } 170 break; 171 case RELATION: 172 data = new RelationData(); 173 List<RelationMemberData> members = ((HistoryRelation) hp).getMembers(); 174 ((RelationData) data).setMembers(members); 175 break; 176 default: throw new AssertionError("Unknown primitive type"); 177 } 178 179 data.setUser(hp.getUser()); 180 try { 181 data.setVisible(hp.isVisible()); 182 } catch (IllegalStateException e) { 183 Main.error("Cannot change visibility for "+p+": "+e.getMessage()); 184 } 185 data.setTimestamp(hp.getTimestamp()); 186 data.setKeys(hp.getTags()); 187 data.setOsmId(hp.getId(), (int) hp.getVersion()); 188 189 // Load the history data 190 try { 191 p.load(data); 192 // Forget this primitive 193 it.remove(); 194 } catch (AssertionError e) { 195 Main.error("Cannot load "+p + ": " + e.getMessage()); 196 } 197 } 198 } 199 } 200 source.removeHistoryDataSetListener(this); 201 if (toLoadNext.isEmpty()) { 202 // No more primitive to update. Processing is finished 203 // Be sure all updated primitives are correctly drawn 204 Main.map.repaint(); 205 } else { 206 // Some primitives still need to be loaded 207 // Let's load all required history 208 Main.worker.submit(new HistoryLoaderAndListener(toLoadNext)); 209 } 210 } 211 212 @Override 213 public void historyDataSetCleared(HistoryDataSet source) { 214 // Do nothing 215 } 216 } 217}