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}