Skip to content

Package: TileCache

TileCache

nameinstructionbranchcomplexitylinemethod
TileCache(MapView.Options)
M: 43 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 10 C: 0
0%
M: 1 C: 0
0%
dispose()
M: 16 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 5 C: 0
0%
M: 1 C: 0
0%
downloadTile(Path, URI)
M: 101 C: 0
0%
M: 3 C: 0
0%
M: 3 C: 0
0%
M: 23 C: 0
0%
M: 1 C: 0
0%
getErrorBody(HttpResponse)
M: 10 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%
getPendingTileCount()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$getErrorBody$4(String)
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$getErrorBody$5(HttpResponse, String)
M: 8 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$new$0(int)
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$retainPendingTiles$1(int, Tile)
M: 8 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$tileLoader$2(Tile)
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$tileLoader$3(Tile, Path)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
loadImageFromCache(Tile, Path)
M: 25 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 5 C: 0
0%
M: 1 C: 0
0%
loadTileInBackground(Tile)
M: 57 C: 0
0%
M: 8 C: 0
0%
M: 5 C: 0
0%
M: 12 C: 0
0%
M: 1 C: 0
0%
resolveCachedTilePath(Tile)
M: 13 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
retainPendingTiles(int)
M: 12 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
static {...}
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
tileLoader()
M: 64 C: 0
0%
M: 8 C: 0
0%
M: 5 C: 0
0%
M: 17 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * *************************************************************************************************************************************************************
3: *
4: * MapView: a JavaFX map renderer for tile-based servers
5: * http://tidalwave.it/projects/mapview
6: *
7: * Copyright (C) 2024 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *************************************************************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12: * You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17: * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18: *
19: * *************************************************************************************************************************************************************
20: *
21: * git clone https://bitbucket.org/tidalwave/mapview-src
22: * git clone https://github.com/tidalwave-it/mapview-src
23: *
24: * *************************************************************************************************************************************************************
25: */
26: package it.tidalwave.mapviewer.javafx.impl;
27:
28: import java.lang.ref.SoftReference;
29: import jakarta.annotation.Nonnull;
30: import java.util.Map;
31: import java.util.Optional;
32: import java.util.concurrent.BlockingQueue;
33: import java.util.concurrent.ConcurrentHashMap;
34: import java.util.concurrent.ExecutorService;
35: import java.util.concurrent.Executors;
36: import java.util.concurrent.LinkedBlockingQueue;
37: import java.util.concurrent.TimeUnit;
38: import java.util.stream.IntStream;
39: import java.nio.charset.StandardCharsets;
40: import java.nio.file.Files;
41: import java.nio.file.Path;
42: import java.net.URI;
43: import java.net.http.HttpClient;
44: import java.net.http.HttpRequest;
45: import java.net.http.HttpResponse;
46: import javafx.scene.image.Image;
47: import javafx.application.Platform;
48: import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49: import it.tidalwave.mapviewer.javafx.MapView;
50: import lombok.extern.slf4j.Slf4j;
51: import static it.tidalwave.mapviewer.impl.NameMangler.mangle;
52: import static java.net.http.HttpClient.Redirect.ALWAYS;
53:
54: /***************************************************************************************************************************************************************
55: *
56: * A cache for tiles.
57: *
58: * @author Fabrizio Giudici
59: *
60: **************************************************************************************************************************************************************/
61: @Slf4j
62: public class TileCache
63: {
64: /** The queue of tiles to be downloaded. */
65: @Nonnull
66: private final BlockingQueue<Tile> tileQueue;
67:
68: /** Options of the map view. */
69: @Nonnull
70: private final MapView.Options options;
71:
72: /** The thread pool for downloading tiles. */
73: @Nonnull
74: private final ExecutorService executorService;
75:
76: /** Whether the downloader thread should be stopped. */
77: private volatile boolean stopped = false;
78:
79: /** This is important to avoid flickering then the TileGrid recreates tiles. */
80: private final Map<URI, SoftReference<Image>> memoryImageCache = new ConcurrentHashMap<>();
81:
82: /** The placeholder used while the tile image has not been loaded yet. */
83: private final Image waitingImage = new Image(TileCache.class.getResource("/hold-on.gif").toExternalForm());
84:
85: /***********************************************************************************************************************************************************
86: *
87: **********************************************************************************************************************************************************/
88: @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
89: public TileCache (@Nonnull final MapView.Options options)
90: {
91: this.options = options;
92: tileQueue = new LinkedBlockingQueue<>(options.tileQueueCapacity());
93: final var poolSize = options.poolSize();
94: executorService = Executors.newFixedThreadPool(poolSize);
95: IntStream.range(0, poolSize).forEach(i -> executorService.submit(this::tileLoader));
96: }
97:
98: /***********************************************************************************************************************************************************
99: * {@return the number of tiles in the download queue}.
100: **********************************************************************************************************************************************************/
101: public int getPendingTileCount()
102: {
103: return tileQueue.size();
104: }
105:
106: /***********************************************************************************************************************************************************
107: * Loads a tile in background.
108: * @param tile the tile to download
109: **********************************************************************************************************************************************************/
110: public final void loadTileInBackground (@Nonnull final Tile tile)
111: {
112: final var imageRef = memoryImageCache.get(tile.getUri());
113:• final var image = (imageRef == null) ? null : imageRef.get();
114:
115:• if (image != null)
116: {
117: tile.setImage(image);
118: }
119: else
120: {
121: final var localPath = resolveCachedTilePath(tile);
122:
123:• if (Files.exists(localPath))
124: {
125: loadImageFromCache(tile, localPath);
126: }
127: else
128: {
129: tile.setImage(waitingImage);
130:
131:• if (tileQueue.offer(tile))
132: {
133: log.debug("Tiles in download queue: {}", tileQueue.size());
134: }
135: else
136: {
137: log.warn("Download queue full, discarding: {}", tile);
138: }
139: }
140: }
141: }
142:
143: /***********************************************************************************************************************************************************
144: * Clears the queue of pending tiles, retaining only those for the given zoom level.
145: * @param zoom the zoom level to retain
146: **********************************************************************************************************************************************************/
147: public void retainPendingTiles (final int zoom)
148: {
149: log.debug("retainPendingTiles({})", zoom);
150:• tileQueue.removeIf(tile -> tile.getZoom() != zoom);
151: }
152:
153: /***********************************************************************************************************************************************************
154: *
155: **********************************************************************************************************************************************************/
156: public void dispose()
157: throws InterruptedException
158: {
159: log.debug("dispose()");
160: stopped = true;
161: executorService.shutdown();
162: executorService.awaitTermination(10, TimeUnit.SECONDS);
163: }
164:
165: /***********************************************************************************************************************************************************
166: * The main loop that downloads the tiles.
167: **********************************************************************************************************************************************************/
168: private void tileLoader()
169: {
170:• while (!stopped)
171: {
172: try
173: {
174: log.debug("waiting for next tile to load... queue size = {}", tileQueue.size());
175: final var tile = tileQueue.take();
176: final var uri = tile.getUri();
177: final var localPath = resolveCachedTilePath(tile);
178:
179:• if (!Files.exists(localPath) && options.downloadAllowed())
180: {
181: downloadTile(localPath, uri);
182: }
183:
184:• if (!Files.exists(localPath))
185: {
186: Platform.runLater(() -> tile.setImage(null));
187: }
188: else
189: {
190: Platform.runLater(() -> loadImageFromCache(tile, localPath));
191: }
192: }
193: catch (InterruptedException ignored)
194: {
195: log.info("tileLoader interrupted");
196: }
197: catch (Exception e) // defensive
198: {
199: log.error("", e);
200: }
201: }
202:
203: log.info("tileLoader terminated");
204: }
205:
206: /***********************************************************************************************************************************************************
207: * Loads an image from the cache.
208: * @param tile the tile
209: * @param path the path of the cache file
210: **********************************************************************************************************************************************************/
211: private void loadImageFromCache (@Nonnull final Tile tile, @Nonnull final Path path)
212: {
213: log.debug("Loading tile from cache: {}", path);
214: final var image = new Image(path.toUri().toString());
215: memoryImageCache.put(tile.getUri(), new SoftReference<>(image));
216: tile.setImage(image);
217: }
218:
219: /***********************************************************************************************************************************************************
220: * {@return the path of the cached tile}.
221: * @param tile the tile
222: **********************************************************************************************************************************************************/
223: @Nonnull
224: private Path resolveCachedTilePath (@Nonnull final Tile tile)
225: {
226: return options.cacheFolder().resolve(tile.getSource().getCachePrefix()).resolve(mangle(tile.getUri().toString()));
227: }
228:
229: /***********************************************************************************************************************************************************
230: * Downloads a tile and stores it.
231: * @param localPath the file to store the tile into
232: * @param uri the uri of the tile
233: **********************************************************************************************************************************************************/
234: @SuppressFBWarnings("REC_CATCH_EXCEPTION")
235: private static void downloadTile (@Nonnull final Path localPath, @Nonnull final URI uri)
236: {
237: try (final var client = HttpClient.newBuilder().followRedirects(ALWAYS).build())
238: {
239: Files.createDirectories(localPath.getParent());
240: final var request = HttpRequest.newBuilder()
241: .GET()
242: .header("User-Agent", "curl/8.7.1")
243: .header("Accept", "*/*")
244: .uri(uri)
245: .build();
246: final var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
247:
248:• switch (response.statusCode())
249: {
250: case 200:
251: final var bytes = response.body();
252: Files.write(localPath, bytes);
253: log.debug("written {} bytes to {}", bytes.length, localPath);
254: break;
255: case 503:
256: log.warn("status code 503 for {}, should re-schedule; {}", uri, response.headers().map());
257: getErrorBody(response).ifPresent(log::warn);
258: // TODO: should reschedule, but not immediately, and also count for a max number of attempts
259: // TOOD: could use a different placeholder image?
260: break;
261: default:
262: log.error("status code {} for {}; {}", response.statusCode(), uri, response.headers().map());
263: getErrorBody(response).ifPresent(log::error);
264: }
265: }
266: catch (Exception e) // defensive
267: {
268: log.error("", e);
269: }
270: }
271:
272: /***********************************************************************************************************************************************************
273: *
274: **********************************************************************************************************************************************************/
275: @Nonnull
276: private static Optional<String> getErrorBody (@Nonnull final HttpResponse<byte[]> response)
277: {
278: return response.headers()
279: .firstValue("Content-type")
280: .filter(ct -> ct.startsWith("text/"))
281: .map(r -> new String(response.body(), StandardCharsets.UTF_8)); // TODO: charset should be get from response
282: }
283: }