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(DownloadParams settings, Bounds downloadArea, ProgressMonitor progressMonitor) {
065        return null;
066    }
067
068    @Override
069    public Future<?> loadUrl(DownloadParams settings, String url, ProgressMonitor progressMonitor) {
070        final Matcher matcher = Pattern.compile(OSM_WEBSITE_PATTERN).matcher(url);
071        if (matcher.matches()) {
072            url = OsmApi.getOsmApi().getBaseUrl() + "changeset/" + Long.parseLong(matcher.group(2)) + "/download";
073        }
074        downloadTask = new DownloadTask(settings, new OsmServerLocationReader(url), progressMonitor);
075        // Extract .osc filename from URL to set the new layer name
076        extractOsmFilename(settings, "https?://.*/(.*\\.osc)", url);
077        return MainApplication.worker.submit(downloadTask);
078    }
079
080    /**
081     * OsmChange download task.
082     */
083    protected class DownloadTask extends DownloadOsmTask.DownloadTask {
084
085        /**
086         * Constructs a new {@code DownloadTask}.
087         * @param settings download settings
088         * @param reader OSM data reader
089         * @param progressMonitor progress monitor
090         */
091        public DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor) {
092            super(settings, reader, progressMonitor);
093        }
094
095        @Override
096        protected DataSet parseDataSet() throws OsmTransferException {
097            return reader.parseOsmChange(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
098        }
099
100        @Override
101        protected void finish() {
102            super.finish();
103            if (isFailed() || isCanceled() || downloadedData == null)
104                return; // user canceled download or error occurred
105            try {
106                // A changeset does not contain all referred primitives, this is the map of incomplete ones
107                // For each incomplete primitive, we'll have to get its state at date it was referred
108                Map<OsmPrimitive, Date> toLoad = new HashMap<>();
109                for (OsmPrimitive p : downloadedData.allNonDeletedPrimitives()) {
110                    if (p.isIncomplete()) {
111                        Date timestamp = null;
112                        for (OsmPrimitive ref : p.getReferrers()) {
113                            if (!ref.isTimestampEmpty()) {
114                                timestamp = ref.getTimestamp();
115                                break;
116                            }
117                        }
118                        toLoad.put(p, timestamp);
119                    }
120                }
121                if (isCanceled()) return;
122                // Let's load all required history
123                MainApplication.worker.submit(new HistoryLoaderAndListener(toLoad));
124            } catch (RejectedExecutionException e) {
125                rememberException(e);
126                setFailed(true);
127            }
128        }
129    }
130
131    /**
132     * Loads history and updates incomplete primitives.
133     */
134    private static final class HistoryLoaderAndListener extends HistoryLoadTask implements HistoryDataSetListener {
135
136        private final Map<OsmPrimitive, Date> toLoad;
137
138        private HistoryLoaderAndListener(Map<OsmPrimitive, Date> toLoad) {
139            this.toLoad = toLoad;
140            this.setChangesetDataNeeded(false);
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}