source: josm/trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java@ 8424

Last change on this file since 8424 was 8424, checked in by wiktorn, 9 years ago

Set URLConnection.setUseCaches(false) when forcing reload of tiles. Closes #11379

File size: 11.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.ByteArrayInputStream;
7import java.io.IOException;
8import java.net.URL;
9import java.util.HashSet;
10import java.util.List;
11import java.util.Map;
12import java.util.Map.Entry;
13import java.util.Set;
14import java.util.concurrent.ConcurrentHashMap;
15import java.util.concurrent.ConcurrentMap;
16import java.util.concurrent.ThreadPoolExecutor;
17import java.util.logging.Level;
18import java.util.logging.Logger;
19
20import org.apache.commons.jcs.access.behavior.ICacheAccess;
21import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
22import org.openstreetmap.gui.jmapviewer.Tile;
23import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
24import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
25import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
26import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
27import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
28import org.openstreetmap.josm.data.cache.CacheEntry;
29import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
30import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
31import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
32
33/**
34 * @author Wiktor Niesiobędzki
35 *
36 * Class bridging TMS requests to JCS cache requests
37 * @since 8168
38 */
39public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
40 private static final Logger log = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
41 private Tile tile;
42 private volatile URL url;
43
44 // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
45 // that way, we reduce calls to tileLoadingFinished, and general CPU load due to surplus Map repaints
46 private static final ConcurrentMap<String,Set<TileLoaderListener>> inProgress = new ConcurrentHashMap<>();
47
48 /**
49 * Constructor for creating a job, to get a specific tile from cache
50 * @param listener
51 * @param tile to be fetched from cache
52 * @param cache object
53 * @param connectTimeout when connecting to remote resource
54 * @param readTimeout when connecting to remote resource
55 * @param headers to be sent together with request
56 * @param downloadExecutor that will be executing the jobs
57 */
58 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
59 ICacheAccess<String, BufferedImageCacheEntry> cache,
60 int connectTimeout, int readTimeout, Map<String, String> headers,
61 ThreadPoolExecutor downloadExecutor) {
62 super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
63 this.tile = tile;
64 if (listener != null) {
65 String deduplicationKey = getCacheKey();
66 synchronized (inProgress) {
67 Set<TileLoaderListener> newListeners = inProgress.get(deduplicationKey);
68 if (newListeners == null) {
69 newListeners = new HashSet<>();
70 inProgress.put(deduplicationKey, newListeners);
71 }
72 newListeners.add(listener);
73 }
74 }
75 }
76
77 @Override
78 public Tile getTile() {
79 return getCachedTile();
80 }
81
82 @Override
83 public String getCacheKey() {
84 if (tile != null)
85 return tile.getKey();
86 return null;
87 }
88
89 /*
90 * this doesn't needs to be synchronized, as it's not that costly to keep only one execution
91 * in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching
92 * data from cache, that's why URL creation is postponed until it's needed
93 *
94 * We need to have static url value for TileLoaderJob, as for some TileSources we might get different
95 * URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection
96 *
97 */
98 @Override
99 public URL getUrl() {
100 if (url == null) {
101 try {
102 synchronized (this) {
103 if (url == null)
104 url = new URL(tile.getUrl());
105 }
106 } catch (IOException e) {
107 log.log(Level.WARNING, "JCS TMS Cache - error creating URL for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
108 log.log(Level.INFO, "Exception: ", e);
109 }
110 }
111 return url;
112 }
113
114 @Override
115 public boolean isObjectLoadable() {
116 if (cacheData != null) {
117 byte[] content = cacheData.getContent();
118 try {
119 return content != null || cacheData.getImage() != null || isNoTileAtZoom();
120 } catch (IOException e) {
121 log.log(Level.WARNING, "JCS TMS - error loading from cache for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
122 }
123 }
124 return false;
125 }
126
127 private boolean isNoTileAtZoom() {
128 if (attributes == null) {
129 log.warning("Cache attributes are null");
130 }
131 return attributes != null && attributes.isNoTileAtZoom();
132 }
133
134 @Override
135 protected boolean cacheAsEmpty(Map<String, List<String>> headers, int statusCode, byte[] content) {
136 // cacheAsEmpty is called for every successful download, so we can put
137 // metadata handling here
138 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
139 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
140 attributes.setNoTileAtZoom(true);
141 return true;
142 }
143 return false;
144 }
145
146 private boolean handleNoTileAtZoom() {
147 if (isNoTileAtZoom()) {
148 log.log(Level.FINE, "JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
149 tile.setError("No tile at this zoom level");
150 tile.putValue("tile-info", "no-tile");
151 return true;
152 }
153 return false;
154 }
155
156 @Override
157 public void submit(boolean force) {
158 tile.initLoading();
159 super.submit(this, force);
160 }
161
162 @Override
163 public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) {
164 this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along
165 Set<TileLoaderListener> listeners;
166 synchronized (inProgress) {
167 listeners = inProgress.remove(getCacheKey());
168 }
169
170 try {
171 if(!tile.isLoaded()) { //if someone else already loaded tile, skip all the handling
172 tile.finishLoading(); // whatever happened set that loading has finished
173 // set tile metadata
174 if (this.attributes != null) {
175 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
176 tile.putValue(e.getKey(), e.getValue());
177 }
178 }
179
180 switch(result){
181 case SUCCESS:
182 handleNoTileAtZoom();
183 if (object != null) {
184 byte[] content = object.getContent();
185 if (content != null && content.length > 0) {
186 tile.loadImage(new ByteArrayInputStream(content));
187 }
188 }
189 int httpStatusCode = attributes.getResponseCode();
190 if (!isNoTileAtZoom() && httpStatusCode >= 400) {
191 tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode));
192 }
193 break;
194 case FAILURE:
195 tile.setError("Problem loading tile");
196 // no break intentional here
197 case CANCELED:
198 // do nothing
199 }
200 }
201
202 // always check, if there is some listener interested in fact, that tile has finished loading
203 if (listeners != null) { // listeners might be null, if some other thread notified already about success
204 for(TileLoaderListener l: listeners) {
205 l.tileLoadingFinished(tile, result.equals(LoadResult.SUCCESS));
206 }
207 }
208 } catch (IOException e) {
209 log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
210 tile.setError(e.getMessage());
211 tile.setLoaded(false);
212 if (listeners != null) { // listeners might be null, if some other thread notified already about success
213 for(TileLoaderListener l: listeners) {
214 l.tileLoadingFinished(tile, false);
215 }
216 }
217 }
218 }
219
220 /**
221 * Method for getting the tile from cache only, without trying to reach remote resource
222 * @return tile or null, if nothing (useful) was found in cache
223 */
224 public Tile getCachedTile() {
225 BufferedImageCacheEntry data = get();
226 if (isObjectLoadable()) {
227 try {
228 // set tile metadata
229 if (this.attributes != null) {
230 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
231 tile.putValue(e.getKey(), e.getValue());
232 }
233 }
234
235 if (data != null && data.getImage() != null) {
236 tile.setImage(data.getImage());
237 tile.finishLoading();
238 }
239 if (isNoTileAtZoom()) {
240 handleNoTileAtZoom();
241 tile.finishLoading();
242 }
243 if (attributes.getResponseCode() >= 400) {
244 tile.setError(tr("HTTP error {0} when loading tiles", attributes.getResponseCode()));
245 }
246 return tile;
247 } catch (IOException e) {
248 log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
249 return null;
250 }
251
252 } else {
253 return tile;
254 }
255 }
256
257 @Override
258 protected boolean handleNotFound() {
259 if (tile.getSource().isNoTileAtZoom(null, 404, null)) {
260 tile.setError("No tile at this zoom level");
261 tile.putValue("tile-info", "no-tile");
262 return true;
263 }
264 return false;
265 }
266
267 /**
268 * For TMS use BaseURL as settings discovery, so for different paths, we will have different settings (useful for developer servers)
269 *
270 * @return base URL of TMS or server url as defined in super class
271 */
272 @Override
273 protected String getServerKey() {
274 TileSource ts = tile.getSource();
275 if (ts instanceof AbstractTMSTileSource) {
276 return ((AbstractTMSTileSource) ts).getBaseUrl();
277 }
278 return super.getServerKey();
279 }
280
281 @Override
282 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
283 return new BufferedImageCacheEntry(content);
284 }
285
286 @Override
287 public void submit() {
288 submit(false);
289 }
290}
Note: See TracBrowser for help on using the repository browser.