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