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

Last change on this file since 11457 was 11452, checked in by Don-vip, 7 years ago

sonar - fb-contrib:SEO_SUBOPTIMAL_EXPRESSION_ORDER - Performance - Method orders expressions in a conditional in a sub optimal way

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