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

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

TileSource:

  • added method - getTileId that returns unique identifier for the tile, that should not collide with other tile sources
  • added JavaDocs

JCSCacheManager:

  • moved from object count limit to object size limit
  • fixed bug with unnecessary re-creation of auxilary cache, that could result in cache corruption/loss of current cache data

CachedTileLoaderFactory, WMSCachedTileLoader, TMSCachedTileLoader

  • un-abstract CachedTileLoaderFactory, use reflection to create TileLoaders
  • adjust constructors

TMSCachedTileLoader, AbstractCachedTileSourceLayer:

  • move cache related settings to AbstractCachedTileSourceLayer
  • move cache instation to AbstractCachedTileSourceLayer
  • make "flush tile cache" command clear only one tile source

TMSCachedTileLoaderJob:

  • make "flush tile cache" command clear only one tile source
  • reorder methods

TemplatedWMSTileSource:

  • java docs
  • inline of private methods: getTileXMax, getTileYMax
  • fix sonar issues
  • make WMS layer zoom levels closer to TMS (addresses: #11459)

WMTSTileSource:

  • fix Sonar issues
  • use topLeftCorner in X/Y tile max calculations instead of world bounds (fixes issues with WMTS-es, for which topLeftCorner lies outside projection world bounds)

AbstractTileSourceLayer:

  • draw warning, when min-zoom-level is set, and tiles are not loaded due to too many tiles on screen

TMSLayer, WMSLayer, WMTSLayer:

  • expose access to cache object for ImageryPreferences

CacheContentsPanel:

  • add panel for managing cache regions and tile sources within the regions

CommonSettingsPanel, TMSSettingsPanel:

  • move settings common to all imagery layers from TMSSettingsPanel to CommonSettingsPanel
File size: 12.7 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 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() {
109 if (url == null) {
110 try {
111 synchronized (this) {
112 if (url == null)
113 url = new URL(tile.getUrl());
114 }
115 } catch (IOException e) {
116 LOG.log(Level.WARNING, "JCS TMS Cache - error creating URL for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
117 LOG.log(Level.INFO, "Exception: ", e);
118 }
119 }
120 return url;
121 }
122
123 @Override
124 public boolean isObjectLoadable() {
125 if (cacheData != null) {
126 byte[] content = cacheData.getContent();
127 try {
128 return content != null || cacheData.getImage() != null || isNoTileAtZoom();
129 } catch (IOException e) {
130 LOG.log(Level.WARNING, "JCS TMS - error loading from cache for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
131 }
132 }
133 return false;
134 }
135
136 @Override
137 protected boolean isResponseLoadable(Map<String, List<String>> headers, int statusCode, byte[] content) {
138 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
139 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
140 attributes.setNoTileAtZoom(true);
141 return false; // do no try to load data from no-tile at zoom, cache empty object instead
142 }
143 return super.isResponseLoadable(headers, statusCode, content);
144 }
145
146 @Override
147 protected boolean cacheAsEmpty() {
148 return isNoTileAtZoom() || super.cacheAsEmpty();
149 }
150
151 @Override
152 public void submit(boolean force) {
153 tile.initLoading();
154 super.submit(this, force);
155 }
156
157 @Override
158 public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) {
159 this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along
160 Set<TileLoaderListener> listeners;
161 synchronized (inProgress) {
162 listeners = inProgress.remove(getCacheKey());
163 }
164 boolean status = result.equals(LoadResult.SUCCESS);
165
166 try {
167 if (!tile.isLoaded()) { //if someone else already loaded tile, skip all the handling
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 if (object != null) {
180 byte[] content = object.getContent();
181 if (content != null && content.length > 0) {
182 tile.loadImage(new ByteArrayInputStream(content));
183 if (tile.getImage() == null) {
184 tile.setError(tr("Could not load image from tile server"));
185 status = false;
186 }
187 }
188 }
189 int httpStatusCode = attributes.getResponseCode();
190 if (!isNoTileAtZoom() && httpStatusCode >= 400) {
191 tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode));
192 status = false;
193 }
194 break;
195 case FAILURE:
196 tile.setError("Problem loading tile");
197 // no break intentional here
198 case CANCELED:
199 // do nothing
200 }
201 }
202
203 // always check, if there is some listener interested in fact, that tile has finished loading
204 if (listeners != null) { // listeners might be null, if some other thread notified already about success
205 for (TileLoaderListener l: listeners) {
206 l.tileLoadingFinished(tile, status);
207 }
208 }
209 } catch (IOException e) {
210 LOG.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
211 tile.setError(e.getMessage());
212 tile.setLoaded(false);
213 if (listeners != null) { // listeners might be null, if some other thread notified already about success
214 for (TileLoaderListener l: listeners) {
215 l.tileLoadingFinished(tile, false);
216 }
217 }
218 }
219 }
220
221 /**
222 * For TMS use BaseURL as settings discovery, so for different paths, we will have different settings (useful for developer servers)
223 *
224 * @return base URL of TMS or server url as defined in super class
225 */
226 @Override
227 protected String getServerKey() {
228 TileSource ts = tile.getSource();
229 if (ts instanceof AbstractTMSTileSource) {
230 return ((AbstractTMSTileSource) ts).getBaseUrl();
231 }
232 return super.getServerKey();
233 }
234
235 @Override
236 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
237 return new BufferedImageCacheEntry(content);
238 }
239
240 @Override
241 public void submit() {
242 submit(false);
243 }
244
245 @Override
246 protected CacheEntryAttributes parseHeaders(URLConnection urlConn) {
247 CacheEntryAttributes ret = super.parseHeaders(urlConn);
248 // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
249 // at least for some short period of time, but not too long
250 if (ret.getExpirationTime() < MINIMUM_EXPIRES) {
251 ret.setExpirationTime(now + MINIMUM_EXPIRES);
252 }
253 if (ret.getExpirationTime() > MAXIMUM_EXPIRES) {
254 ret.setExpirationTime(now + MAXIMUM_EXPIRES);
255 }
256 return ret;
257 }
258
259 /**
260 * Method for getting the tile from cache only, without trying to reach remote resource
261 * @return tile or null, if nothing (useful) was found in cache
262 */
263 public Tile getCachedTile() {
264 BufferedImageCacheEntry data = get();
265 if (isObjectLoadable()) {
266 try {
267 // set tile metadata
268 if (this.attributes != null) {
269 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
270 tile.putValue(e.getKey(), e.getValue());
271 }
272 }
273
274 if (data != null) {
275 if (data.getImage() != null) {
276 tile.setImage(data.getImage());
277 tile.finishLoading();
278 } else {
279 // we had some data, but we didn't get any image. Malformed image?
280 tile.setError(tr("Could not load image from tile server"));
281 }
282 }
283 if (isNoTileAtZoom()) {
284 handleNoTileAtZoom();
285 tile.finishLoading();
286 }
287 if (attributes.getResponseCode() >= 400) {
288 tile.setError(tr("HTTP error {0} when loading tiles", attributes.getResponseCode()));
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
319}
Note: See TracBrowser for help on using the repository browser.