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