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

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

sonar - fb-contrib - minor performance improvements:

  • Method passes constant String of length 1 to character overridden method
  • Method needlessly boxes a boolean constant
  • Method uses iterator().next() on a List to get the first item
  • Method converts String to boxed primitive using excessive boxing
  • Method converts String to primitive using excessive boxing
  • Method creates array using constants
  • Class defines List based fields but uses them like Sets
  • Property svn:eol-style set to native
File size: 13.2 KB
RevLine 
[8168]1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
[8389]4import static org.openstreetmap.josm.tools.I18n.tr;
5
[8168]6import java.io.ByteArrayInputStream;
7import java.io.IOException;
8import java.net.URL;
[8425]9import java.net.URLConnection;
[8326]10import java.util.HashSet;
[8344]11import java.util.List;
[8168]12import java.util.Map;
[8418]13import java.util.Map.Entry;
[8326]14import java.util.Set;
[8307]15import java.util.concurrent.ConcurrentHashMap;
[8326]16import java.util.concurrent.ConcurrentMap;
[8403]17import java.util.concurrent.ThreadPoolExecutor;
[8168]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;
[8344]30import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
[8168]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
[8318]38 * @since 8168
[8168]39 */
40public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
[8425]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*/;
[8168]44 private Tile tile;
45 private volatile URL url;
46
[8326]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
[8510]49 private static final ConcurrentMap<String, Set<TileLoaderListener>> inProgress = new ConcurrentHashMap<>();
[8326]50
[8168]51 /**
52 * Constructor for creating a job, to get a specific tile from cache
[8459]53 * @param listener Tile loader listener
[8168]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
[8459]58 * @param headers HTTP headers to be sent together with request
[8403]59 * @param downloadExecutor that will be executing the jobs
[8168]60 */
[8397]61 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
62 ICacheAccess<String, BufferedImageCacheEntry> cache,
63 int connectTimeout, int readTimeout, Map<String, String> headers,
[8403]64 ThreadPoolExecutor downloadExecutor) {
[8397]65 super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
[8168]66 this.tile = tile;
[8326]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 }
[8168]78 }
79
80 @Override
81 public Tile getTile() {
82 return getCachedTile();
83 }
84
85 @Override
86 public String getCacheKey() {
[8598]87 if (tile != null) {
88 TileSource tileSource = tile.getTileSource();
89 String tsName = tileSource.getName();
90 if (tsName == null) {
91 tsName = "";
92 }
[8846]93 return tsName.replace(':', '_') + ':' + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile());
[8598]94 }
[8168]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
[8314]101 * data from cache, that's why URL creation is postponed until it's needed
[8168]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
[8673]108 public URL getUrl() throws IOException {
[8168]109 if (url == null) {
[8673]110 synchronized (this) {
111 if (url == null)
112 url = new URL(tile.getUrl());
[8168]113 }
114 }
115 return url;
116 }
117
118 @Override
119 public boolean isObjectLoadable() {
120 if (cacheData != null) {
121 byte[] content = cacheData.getContent();
122 try {
[8344]123 return content != null || cacheData.getImage() != null || isNoTileAtZoom();
[8168]124 } catch (IOException e) {
[8425]125 LOG.log(Level.WARNING, "JCS TMS - error loading from cache for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
[8168]126 }
127 }
128 return false;
129 }
130
131 @Override
[8433]132 protected boolean isResponseLoadable(Map<String, List<String>> headers, int statusCode, byte[] content) {
[8418]133 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
[8344]134 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
135 attributes.setNoTileAtZoom(true);
[8433]136 return false; // do no try to load data from no-tile at zoom, cache empty object instead
[8344]137 }
[8433]138 return super.isResponseLoadable(headers, statusCode, content);
[8176]139 }
140
[8433]141 @Override
142 protected boolean cacheAsEmpty() {
143 return isNoTileAtZoom() || super.cacheAsEmpty();
144 }
145
[8424]146 @Override
147 public void submit(boolean force) {
[8168]148 tile.initLoading();
[8673]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 }
[8168]156 }
157
158 @Override
[8344]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
[8326]161 Set<TileLoaderListener> listeners;
162 synchronized (inProgress) {
163 listeners = inProgress.remove(getCacheKey());
164 }
[8433]165 boolean status = result.equals(LoadResult.SUCCESS);
[8326]166
[8168]167 try {
[8326]168 tile.finishLoading(); // whatever happened set that loading has finished
[8418]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
[8510]176 switch(result) {
[8326]177 case SUCCESS:
178 handleNoTileAtZoom();
[8389]179 int httpStatusCode = attributes.getResponseCode();
[8643]180 if (!isNoTileAtZoom() && httpStatusCode >= 400) {
[8606]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 }
[8433]186 status = false;
[8389]187 }
[8815]188 status &= tryLoadTileImage(object); //try to keep returned image as background
[8403]189 break;
190 case FAILURE:
191 tile.setError("Problem loading tile");
[8815]192 tryLoadTileImage(object);
[8768]193 break;
[8403]194 case CANCELED:
[8768]195 tile.loadingCanceled();
[8326]196 // do nothing
[8176]197 }
[8326]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
[8510]201 for (TileLoaderListener l: listeners) {
[8433]202 l.tileLoadingFinished(tile, status);
[8326]203 }
[8168]204 }
205 } catch (IOException e) {
[8425]206 LOG.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
[8606]207 tile.setError(e.toString());
[8168]208 tile.setLoaded(false);
[8326]209 if (listeners != null) { // listeners might be null, if some other thread notified already about success
[8510]210 for (TileLoaderListener l: listeners) {
[8326]211 l.tileLoadingFinished(tile, false);
212 }
[8168]213 }
214 }
215 }
216
217 /**
[8598]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
[8636]246 if (ret.getExpirationTime() < now + MINIMUM_EXPIRES) {
[8598]247 ret.setExpirationTime(now + MINIMUM_EXPIRES);
248 }
[8636]249 if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES) {
[8598]250 ret.setExpirationTime(now + MAXIMUM_EXPIRES);
251 }
252 return ret;
253 }
254
255 /**
[8168]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() {
[8174]260 BufferedImageCacheEntry data = get();
[8640]261 if (isObjectLoadable() && isCacheElementValid()) {
[8168]262 try {
[8418]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
[8488]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 }
[8176]278 }
279 if (isNoTileAtZoom()) {
280 handleNoTileAtZoom();
281 tile.finishLoading();
282 }
[8635]283 if (attributes != null && attributes.getResponseCode() >= 400) {
[8607]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 }
[8389]289 }
[8168]290 return tile;
291 } catch (IOException e) {
[8425]292 LOG.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
[8168]293 return null;
294 }
295
296 } else {
[8176]297 return tile;
[8168]298 }
299 }
300
[8598]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;
[8168]307 }
[8598]308 return false;
[8168]309 }
310
[8598]311 private boolean isNoTileAtZoom() {
312 if (attributes == null) {
313 LOG.warning("Cache attributes are null");
314 }
315 return attributes != null && attributes.isNoTileAtZoom();
[8168]316 }
[8424]317
[8815]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 }
[8168]331}
Note: See TracBrowser for help on using the repository browser.