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

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

addresses #11437 - introduce infinite queue for tile loading and clear the queue when user pans the map or changes the zoom. Fixing hosts limit is last problem, thay may incur additional load

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