001// License: GPL. For details, see Readme.txt file.
002package org.openstreetmap.gui.jmapviewer;
003
004import static org.openstreetmap.gui.jmapviewer.FeatureAdapter.tr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.net.HttpURLConnection;
009import java.net.URL;
010import java.net.URLConnection;
011import java.util.HashMap;
012import java.util.Map;
013import java.util.Map.Entry;
014import java.util.concurrent.Executors;
015import java.util.concurrent.ThreadPoolExecutor;
016
017import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
018import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
019import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
020
021/**
022 * A {@link TileLoader} implementation that loads tiles from OSM.
023 *
024 * @author Jan Peter Stotz
025 */
026public class OsmTileLoader implements TileLoader {
027    private static final ThreadPoolExecutor jobDispatcher = (ThreadPoolExecutor) Executors.newFixedThreadPool(8);
028
029    private final class OsmTileJob implements TileJob {
030        private final Tile tile;
031        private InputStream input;
032        private boolean force;
033
034        private OsmTileJob(Tile tile) {
035            this.tile = tile;
036        }
037
038        @Override
039        public void run() {
040            synchronized (tile) {
041                if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
042                    return;
043                tile.loaded = false;
044                tile.error = false;
045                tile.loading = true;
046            }
047            try {
048                URLConnection conn = loadTileFromOsm(tile);
049                if (force) {
050                    conn.setUseCaches(false);
051                }
052                loadTileMetadata(tile, conn);
053                if ("no-tile".equals(tile.getValue("tile-info"))) {
054                    tile.setError(tr("No tiles at this zoom level"));
055                } else {
056                    input = conn.getInputStream();
057                    try {
058                        tile.loadImage(input);
059                    } finally {
060                        input.close();
061                        input = null;
062                    }
063                }
064                tile.setLoaded(true);
065                listener.tileLoadingFinished(tile, true);
066            } catch (IOException e) {
067                tile.setError(e.getMessage());
068                listener.tileLoadingFinished(tile, false);
069                if (input == null) {
070                    try {
071                        System.err.println("Failed loading " + tile.getUrl() +": "
072                                +e.getClass() + ": " + e.getMessage());
073                    } catch (IOException ioe) {
074                        ioe.printStackTrace();
075                    }
076                }
077            } finally {
078                tile.loading = false;
079                tile.setLoaded(true);
080            }
081        }
082
083        @Override
084        public void submit() {
085            submit(false);
086        }
087
088        @Override
089        public void submit(boolean force) {
090            this.force = force;
091            jobDispatcher.execute(this);
092        }
093    }
094
095    /**
096     * Holds the HTTP headers. Insert e.g. User-Agent here when default should not be used.
097     */
098    public Map<String, String> headers = new HashMap<>();
099
100    public int timeoutConnect;
101    public int timeoutRead;
102
103    protected TileLoaderListener listener;
104
105    public OsmTileLoader(TileLoaderListener listener) {
106        this(listener, null);
107    }
108
109    public OsmTileLoader(TileLoaderListener listener, Map<String, String> headers) {
110        this.headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
111        if (headers != null) {
112            this.headers.putAll(headers);
113        }
114        this.listener = listener;
115    }
116
117    @Override
118    public TileJob createTileLoaderJob(final Tile tile) {
119        return new OsmTileJob(tile);
120    }
121
122    protected URLConnection loadTileFromOsm(Tile tile) throws IOException {
123        URL url;
124        url = new URL(tile.getUrl());
125        URLConnection urlConn = url.openConnection();
126        if (urlConn instanceof HttpURLConnection) {
127            prepareHttpUrlConnection((HttpURLConnection) urlConn);
128        }
129        return urlConn;
130    }
131
132    protected void loadTileMetadata(Tile tile, URLConnection urlConn) {
133        String str = urlConn.getHeaderField("X-VE-TILEMETA-CaptureDatesRange");
134        if (str != null) {
135            tile.putValue("capture-date", str);
136        }
137        str = urlConn.getHeaderField("X-VE-Tile-Info");
138        if (str != null) {
139            tile.putValue("tile-info", str);
140        }
141
142        Long lng = urlConn.getExpiration();
143        if (lng.equals(0L)) {
144            try {
145                str = urlConn.getHeaderField("Cache-Control");
146                if (str != null) {
147                    for (String token: str.split(",")) {
148                        if (token.startsWith("max-age=")) {
149                            lng = Long.parseLong(token.substring(8)) * 1000 +
150                                    System.currentTimeMillis();
151                        }
152                    }
153                }
154            } catch (NumberFormatException e) {
155                // ignore malformed Cache-Control headers
156                if (JMapViewer.debug) {
157                    System.err.println(e.getMessage());
158                }
159            }
160        }
161        if (!lng.equals(0L)) {
162            tile.putValue("expires", lng.toString());
163        }
164    }
165
166    protected void prepareHttpUrlConnection(HttpURLConnection urlConn) {
167        for (Entry<String, String> e : headers.entrySet()) {
168            urlConn.setRequestProperty(e.getKey(), e.getValue());
169        }
170        if (timeoutConnect != 0)
171            urlConn.setConnectTimeout(timeoutConnect);
172        if (timeoutRead != 0)
173            urlConn.setReadTimeout(timeoutRead);
174    }
175
176    @Override
177    public String toString() {
178        return getClass().getSimpleName();
179    }
180
181    @Override
182    public boolean hasOutstandingTasks() {
183        return jobDispatcher.getTaskCount() > jobDispatcher.getCompletedTaskCount();
184    }
185
186    @Override
187    public void cancelOutstandingTasks() {
188        jobDispatcher.getQueue().clear();
189    }
190
191    /**
192     * Sets the maximum number of concurrent connections the tile loader will do
193     * @param num number of concurrent connections
194     */
195    public static void setConcurrentConnections(int num) {
196        jobDispatcher.setMaximumPoolSize(num);
197    }
198}