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(e, "Cannot change visibility for "+p+':');
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(e, "Cannot load "+p+':');
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}