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

Last change on this file since 9067 was 9067, checked in by Don-vip, 8 years ago

sonar - Immutable Field

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