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

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

see #11390 - SonarQube - squid:S3824 - "Map.get" and value test should be replaced with single method call

  • Property svn:eol-style set to native
File size: 11.3 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.Optional;
14import java.util.Set;
15import java.util.concurrent.ConcurrentHashMap;
16import java.util.concurrent.ConcurrentMap;
17import java.util.concurrent.ThreadPoolExecutor;
18import java.util.concurrent.TimeUnit;
19
20import org.apache.commons.jcs.access.behavior.ICacheAccess;
21import org.openstreetmap.gui.jmapviewer.Tile;
22import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
23import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
24import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
25import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
26import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
27import org.openstreetmap.josm.data.cache.CacheEntry;
28import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
29import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
30import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
31import org.openstreetmap.josm.data.preferences.LongProperty;
32import org.openstreetmap.josm.tools.HttpClient;
33import org.openstreetmap.josm.tools.Logging;
34
35/**
36 * Class bridging TMS requests to JCS cache requests
37 *
38 * @author Wiktor Niesiobędzki
39 * @since 8168
40 */
41public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
42 private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
43 private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
44 protected 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 inProgress.computeIfAbsent(deduplicationKey, k -> new HashSet<>()).add(listener);
71 }
72 }
73 }
74
75 @Override
76 public String getCacheKey() {
77 if (tile != null) {
78 TileSource tileSource = tile.getTileSource();
79 return Optional.ofNullable(tileSource.getName()).orElse("").replace(':', '_') + ':'
80 + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile());
81 }
82 return null;
83 }
84
85 /*
86 * this doesn't needs to be synchronized, as it's not that costly to keep only one execution
87 * in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching
88 * data from cache, that's why URL creation is postponed until it's needed
89 *
90 * We need to have static url value for TileLoaderJob, as for some TileSources we might get different
91 * URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection
92 *
93 */
94 @Override
95 public URL getUrl() throws IOException {
96 if (url == null) {
97 synchronized (this) {
98 if (url == null) {
99 String sUrl = tile.getUrl();
100 if (!"".equals(sUrl)) {
101 url = new URL(sUrl);
102 }
103 }
104 }
105 }
106 return url;
107 }
108
109 @Override
110 public boolean isObjectLoadable() {
111 if (cacheData != null) {
112 byte[] content = cacheData.getContent();
113 try {
114 return content.length > 0 || cacheData.getImage() != null || isNoTileAtZoom();
115 } catch (IOException e) {
116 Logging.logWithStackTrace(Logging.LEVEL_WARN, e, "JCS TMS - error loading from cache for tile {0}: {1}",
117 new Object[] {tile.getKey(), e.getMessage()}
118 );
119 }
120 }
121 return false;
122 }
123
124 @Override
125 protected boolean isResponseLoadable(Map<String, List<String>> headers, int statusCode, byte[] content) {
126 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
127 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
128 attributes.setNoTileAtZoom(true);
129 return false; // do no try to load data from no-tile at zoom, cache empty object instead
130 }
131 return super.isResponseLoadable(headers, statusCode, content);
132 }
133
134 @Override
135 protected boolean cacheAsEmpty() {
136 return isNoTileAtZoom() || super.cacheAsEmpty();
137 }
138
139 @Override
140 public void submit(boolean force) {
141 tile.initLoading();
142 try {
143 super.submit(this, force);
144 } catch (IOException | IllegalArgumentException e) {
145 // if we fail to submit the job, mark tile as loaded and set error message
146 Logging.log(Logging.LEVEL_WARN, e);
147 tile.finishLoading();
148 tile.setError(e.getMessage());
149 }
150 }
151
152 @Override
153 public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) {
154 this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along
155 Set<TileLoaderListener> listeners;
156 synchronized (inProgress) {
157 listeners = inProgress.remove(getCacheKey());
158 }
159 boolean status = result.equals(LoadResult.SUCCESS);
160
161 try {
162 tile.finishLoading(); // whatever happened set that loading has finished
163 // set tile metadata
164 if (this.attributes != null) {
165 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
166 tile.putValue(e.getKey(), e.getValue());
167 }
168 }
169
170 switch(result) {
171 case SUCCESS:
172 handleNoTileAtZoom();
173 if (attributes != null) {
174 int httpStatusCode = attributes.getResponseCode();
175 if (httpStatusCode >= 400 && !isNoTileAtZoom()) {
176 if (attributes.getErrorMessage() == null) {
177 tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode));
178 } else {
179 tile.setError(tr("Error downloading tiles: {0}", attributes.getErrorMessage()));
180 }
181 status = false;
182 }
183 }
184 status &= tryLoadTileImage(object); //try to keep returned image as background
185 break;
186 case FAILURE:
187 tile.setError("Problem loading tile");
188 tryLoadTileImage(object);
189 break;
190 case CANCELED:
191 tile.loadingCanceled();
192 // do nothing
193 }
194
195 // always check, if there is some listener interested in fact, that tile has finished loading
196 if (listeners != null) { // listeners might be null, if some other thread notified already about success
197 for (TileLoaderListener l: listeners) {
198 l.tileLoadingFinished(tile, status);
199 }
200 }
201 } catch (IOException e) {
202 Logging.warn("JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
203 tile.setError(e);
204 tile.setLoaded(false);
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, false);
208 }
209 }
210 }
211 }
212
213 /**
214 * For TMS use BaseURL as settings discovery, so for different paths, we will have different settings (useful for developer servers)
215 *
216 * @return base URL of TMS or server url as defined in super class
217 */
218 @Override
219 protected String getServerKey() {
220 TileSource ts = tile.getSource();
221 if (ts instanceof AbstractTMSTileSource) {
222 return ((AbstractTMSTileSource) ts).getBaseUrl();
223 }
224 return super.getServerKey();
225 }
226
227 @Override
228 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
229 return new BufferedImageCacheEntry(content);
230 }
231
232 @Override
233 public void submit() {
234 submit(false);
235 }
236
237 @Override
238 protected CacheEntryAttributes parseHeaders(HttpClient.Response urlConn) {
239 CacheEntryAttributes ret = super.parseHeaders(urlConn);
240 // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
241 // at least for some short period of time, but not too long
242 if (ret.getExpirationTime() < now + MINIMUM_EXPIRES.get()) {
243 ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
244 }
245 if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES.get()) {
246 ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
247 }
248 return ret;
249 }
250
251 private boolean handleNoTileAtZoom() {
252 if (isNoTileAtZoom()) {
253 Logging.debug("JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
254 tile.setError("No tile at this zoom level");
255 tile.putValue("tile-info", "no-tile");
256 return true;
257 }
258 return false;
259 }
260
261 private boolean isNoTileAtZoom() {
262 if (attributes == null) {
263 Logging.warn("Cache attributes are null");
264 }
265 return attributes != null && attributes.isNoTileAtZoom();
266 }
267
268 private boolean tryLoadTileImage(CacheEntry object) throws IOException {
269 if (object != null) {
270 byte[] content = object.getContent();
271 if (content.length > 0) {
272 tile.loadImage(new ByteArrayInputStream(content));
273 if (tile.getImage() == null) {
274 tile.setError(tr("Could not load image from tile server"));
275 return false;
276 }
277 }
278 }
279 return true;
280 }
281}
Note: See TracBrowser for help on using the repository browser.