1 | // License: GPL. For details, see LICENSE file. |
---|
2 | package org.openstreetmap.josm.gui.layer; |
---|
3 | |
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr; |
---|
5 | |
---|
6 | import java.awt.Color; |
---|
7 | import java.awt.Dimension; |
---|
8 | import java.awt.Font; |
---|
9 | import java.awt.Graphics; |
---|
10 | import java.awt.Graphics2D; |
---|
11 | import java.awt.GridBagLayout; |
---|
12 | import java.awt.Image; |
---|
13 | import java.awt.Point; |
---|
14 | import java.awt.Shape; |
---|
15 | import java.awt.Toolkit; |
---|
16 | import java.awt.event.ActionEvent; |
---|
17 | import java.awt.event.MouseAdapter; |
---|
18 | import java.awt.event.MouseEvent; |
---|
19 | import java.awt.geom.AffineTransform; |
---|
20 | import java.awt.geom.Point2D; |
---|
21 | import java.awt.geom.Rectangle2D; |
---|
22 | import java.awt.image.BufferedImage; |
---|
23 | import java.awt.image.ImageObserver; |
---|
24 | import java.io.File; |
---|
25 | import java.io.IOException; |
---|
26 | import java.net.MalformedURLException; |
---|
27 | import java.net.URL; |
---|
28 | import java.text.SimpleDateFormat; |
---|
29 | import java.util.ArrayList; |
---|
30 | import java.util.Arrays; |
---|
31 | import java.util.Collection; |
---|
32 | import java.util.Collections; |
---|
33 | import java.util.Comparator; |
---|
34 | import java.util.Date; |
---|
35 | import java.util.LinkedList; |
---|
36 | import java.util.List; |
---|
37 | import java.util.Map; |
---|
38 | import java.util.Map.Entry; |
---|
39 | import java.util.Objects; |
---|
40 | import java.util.Set; |
---|
41 | import java.util.concurrent.ConcurrentSkipListSet; |
---|
42 | import java.util.concurrent.atomic.AtomicInteger; |
---|
43 | import java.util.function.Consumer; |
---|
44 | import java.util.function.Function; |
---|
45 | import java.util.stream.Collectors; |
---|
46 | import java.util.stream.IntStream; |
---|
47 | import java.util.stream.Stream; |
---|
48 | |
---|
49 | import javax.swing.AbstractAction; |
---|
50 | import javax.swing.Action; |
---|
51 | import javax.swing.JLabel; |
---|
52 | import javax.swing.JMenuItem; |
---|
53 | import javax.swing.JOptionPane; |
---|
54 | import javax.swing.JPanel; |
---|
55 | import javax.swing.JPopupMenu; |
---|
56 | import javax.swing.JSeparator; |
---|
57 | import javax.swing.Timer; |
---|
58 | |
---|
59 | import org.openstreetmap.gui.jmapviewer.AttributionSupport; |
---|
60 | import org.openstreetmap.gui.jmapviewer.MemoryTileCache; |
---|
61 | import org.openstreetmap.gui.jmapviewer.OsmTileLoader; |
---|
62 | import org.openstreetmap.gui.jmapviewer.Tile; |
---|
63 | import org.openstreetmap.gui.jmapviewer.TileRange; |
---|
64 | import org.openstreetmap.gui.jmapviewer.TileXY; |
---|
65 | import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; |
---|
66 | import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; |
---|
67 | import org.openstreetmap.gui.jmapviewer.interfaces.IProjected; |
---|
68 | import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; |
---|
69 | import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; |
---|
70 | import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; |
---|
71 | import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; |
---|
72 | import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; |
---|
73 | import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; |
---|
74 | import org.openstreetmap.josm.Main; |
---|
75 | import org.openstreetmap.josm.actions.ExpertToggleAction; |
---|
76 | import org.openstreetmap.josm.actions.ImageryAdjustAction; |
---|
77 | import org.openstreetmap.josm.actions.RenameLayerAction; |
---|
78 | import org.openstreetmap.josm.actions.SaveActionBase; |
---|
79 | import org.openstreetmap.josm.data.Bounds; |
---|
80 | import org.openstreetmap.josm.data.ProjectionBounds; |
---|
81 | import org.openstreetmap.josm.data.coor.EastNorth; |
---|
82 | import org.openstreetmap.josm.data.coor.LatLon; |
---|
83 | import org.openstreetmap.josm.data.imagery.CoordinateConversion; |
---|
84 | import org.openstreetmap.josm.data.imagery.ImageryInfo; |
---|
85 | import org.openstreetmap.josm.data.imagery.OffsetBookmark; |
---|
86 | import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; |
---|
87 | import org.openstreetmap.josm.data.imagery.TileLoaderFactory; |
---|
88 | import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; |
---|
89 | import org.openstreetmap.josm.data.preferences.IntegerProperty; |
---|
90 | import org.openstreetmap.josm.data.projection.Projection; |
---|
91 | import org.openstreetmap.josm.data.projection.Projections; |
---|
92 | import org.openstreetmap.josm.gui.ExtendedDialog; |
---|
93 | import org.openstreetmap.josm.gui.MainApplication; |
---|
94 | import org.openstreetmap.josm.gui.MapView; |
---|
95 | import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; |
---|
96 | import org.openstreetmap.josm.gui.Notification; |
---|
97 | import org.openstreetmap.josm.gui.dialogs.LayerListDialog; |
---|
98 | import org.openstreetmap.josm.gui.dialogs.LayerListPopup; |
---|
99 | import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; |
---|
100 | import org.openstreetmap.josm.gui.layer.imagery.AutoLoadTilesAction; |
---|
101 | import org.openstreetmap.josm.gui.layer.imagery.AutoZoomAction; |
---|
102 | import org.openstreetmap.josm.gui.layer.imagery.DecreaseZoomAction; |
---|
103 | import org.openstreetmap.josm.gui.layer.imagery.FlushTileCacheAction; |
---|
104 | import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; |
---|
105 | import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction; |
---|
106 | import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction; |
---|
107 | import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; |
---|
108 | import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; |
---|
109 | import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; |
---|
110 | import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; |
---|
111 | import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter; |
---|
112 | import org.openstreetmap.josm.gui.layer.imagery.TilePosition; |
---|
113 | import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; |
---|
114 | import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; |
---|
115 | import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; |
---|
116 | import org.openstreetmap.josm.gui.layer.imagery.ZoomToBestAction; |
---|
117 | import org.openstreetmap.josm.gui.layer.imagery.ZoomToNativeLevelAction; |
---|
118 | import org.openstreetmap.josm.gui.progress.ProgressMonitor; |
---|
119 | import org.openstreetmap.josm.gui.util.GuiHelper; |
---|
120 | import org.openstreetmap.josm.tools.GBC; |
---|
121 | import org.openstreetmap.josm.tools.HttpClient; |
---|
122 | import org.openstreetmap.josm.tools.Logging; |
---|
123 | import org.openstreetmap.josm.tools.MemoryManager; |
---|
124 | import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; |
---|
125 | import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; |
---|
126 | import org.openstreetmap.josm.tools.Utils; |
---|
127 | |
---|
128 | /** |
---|
129 | * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS |
---|
130 | * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc. |
---|
131 | * |
---|
132 | * @author Upliner |
---|
133 | * @author Wiktor Niesiobędzki |
---|
134 | * @param <T> Tile Source class used for this layer |
---|
135 | * @since 3715 |
---|
136 | * @since 8526 (copied from TMSLayer) |
---|
137 | */ |
---|
138 | public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer |
---|
139 | implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener { |
---|
140 | private static final String PREFERENCE_PREFIX = "imagery.generic"; |
---|
141 | static { // Registers all setting properties |
---|
142 | new TileSourceDisplaySettings(); |
---|
143 | } |
---|
144 | |
---|
145 | /** maximum zoom level supported */ |
---|
146 | public static final int MAX_ZOOM = 30; |
---|
147 | /** minium zoom level supported */ |
---|
148 | public static final int MIN_ZOOM = 2; |
---|
149 | private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); |
---|
150 | |
---|
151 | /** additional layer menu actions */ |
---|
152 | private static List<MenuAddition> menuAdditions = new LinkedList<>(); |
---|
153 | |
---|
154 | /** minimum zoom level to show to user */ |
---|
155 | public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); |
---|
156 | /** maximum zoom level to show to user */ |
---|
157 | public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); |
---|
158 | |
---|
159 | //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); |
---|
160 | /** Zoomlevel at which tiles is currently downloaded. Initial zoom lvl is set to bestZoom */ |
---|
161 | private int currentZoomLevel; |
---|
162 | |
---|
163 | private final AttributionSupport attribution = new AttributionSupport(); |
---|
164 | private final TileHolder clickedTileHolder = new TileHolder(); |
---|
165 | |
---|
166 | /** |
---|
167 | * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in |
---|
168 | * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution |
---|
169 | */ |
---|
170 | public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); |
---|
171 | |
---|
172 | /* |
---|
173 | * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image) |
---|
174 | * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible |
---|
175 | * in MapView (for example - when limiting min zoom in imagery) |
---|
176 | * |
---|
177 | * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached |
---|
178 | */ |
---|
179 | protected TileCache tileCache; // initialized together with tileSource |
---|
180 | protected T tileSource; |
---|
181 | protected TileLoader tileLoader; |
---|
182 | |
---|
183 | /** A timer that is used to delay invalidation events if required. */ |
---|
184 | private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate()); |
---|
185 | |
---|
186 | private final MouseAdapter adapter = new MouseAdapter() { |
---|
187 | @Override |
---|
188 | public void mouseClicked(MouseEvent e) { |
---|
189 | if (!isVisible()) return; |
---|
190 | if (e.getButton() == MouseEvent.BUTTON3) { |
---|
191 | clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY())); |
---|
192 | new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY()); |
---|
193 | } else if (e.getButton() == MouseEvent.BUTTON1) { |
---|
194 | attribution.handleAttribution(e.getPoint(), true); |
---|
195 | } |
---|
196 | } |
---|
197 | }; |
---|
198 | |
---|
199 | private final TileSourceDisplaySettings displaySettings = createDisplaySettings(); |
---|
200 | |
---|
201 | private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this); |
---|
202 | // prepared to be moved to the painter |
---|
203 | protected TileCoordinateConverter coordinateConverter; |
---|
204 | |
---|
205 | /** |
---|
206 | * Creates Tile Source based Imagery Layer based on Imagery Info |
---|
207 | * @param info imagery info |
---|
208 | */ |
---|
209 | public AbstractTileSourceLayer(ImageryInfo info) { |
---|
210 | super(info); |
---|
211 | setBackgroundLayer(true); |
---|
212 | this.setVisible(true); |
---|
213 | getFilterSettings().addFilterChangeListener(this); |
---|
214 | getDisplaySettings().addSettingsChangeListener(this); |
---|
215 | } |
---|
216 | |
---|
217 | /** |
---|
218 | * This method creates the {@link TileSourceDisplaySettings} object. Subclasses may implement it to e.g. change the prefix. |
---|
219 | * @return The object. |
---|
220 | * @since 10568 |
---|
221 | */ |
---|
222 | protected TileSourceDisplaySettings createDisplaySettings() { |
---|
223 | return new TileSourceDisplaySettings(); |
---|
224 | } |
---|
225 | |
---|
226 | /** |
---|
227 | * Gets the {@link TileSourceDisplaySettings} instance associated with this tile source. |
---|
228 | * @return The tile source display settings |
---|
229 | * @since 10568 |
---|
230 | */ |
---|
231 | public TileSourceDisplaySettings getDisplaySettings() { |
---|
232 | return displaySettings; |
---|
233 | } |
---|
234 | |
---|
235 | @Override |
---|
236 | public void filterChanged() { |
---|
237 | invalidate(); |
---|
238 | } |
---|
239 | |
---|
240 | protected abstract TileLoaderFactory getTileLoaderFactory(); |
---|
241 | |
---|
242 | /** |
---|
243 | * Get projections this imagery layer supports natively. |
---|
244 | * |
---|
245 | * For example projection of tiles that are downloaded from a server. Layer |
---|
246 | * may support even more projections (by reprojecting the tiles), but with a |
---|
247 | * certain loss in image quality and performance. |
---|
248 | * @return projections this imagery layer supports natively; null if layer is projection agnostic. |
---|
249 | */ |
---|
250 | public abstract Collection<String> getNativeProjections(); |
---|
251 | |
---|
252 | /** |
---|
253 | * Creates and returns a new {@link TileSource} instance depending on {@link #info} specified in the constructor. |
---|
254 | * |
---|
255 | * @return TileSource for specified ImageryInfo |
---|
256 | * @throws IllegalArgumentException when Imagery is not supported by layer |
---|
257 | */ |
---|
258 | protected abstract T getTileSource(); |
---|
259 | |
---|
260 | protected Map<String, String> getHeaders(T tileSource) { |
---|
261 | if (tileSource instanceof TemplatedTileSource) { |
---|
262 | return ((TemplatedTileSource) tileSource).getHeaders(); |
---|
263 | } |
---|
264 | return null; |
---|
265 | } |
---|
266 | |
---|
267 | protected void initTileSource(T tileSource) { |
---|
268 | coordinateConverter = new TileCoordinateConverter(MainApplication.getMap().mapView, tileSource, getDisplaySettings()); |
---|
269 | attribution.initialize(tileSource); |
---|
270 | |
---|
271 | currentZoomLevel = getBestZoom(); |
---|
272 | |
---|
273 | Map<String, String> headers = getHeaders(tileSource); |
---|
274 | |
---|
275 | tileLoader = getTileLoaderFactory().makeTileLoader(this, headers); |
---|
276 | |
---|
277 | try { |
---|
278 | if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) { |
---|
279 | tileLoader = new OsmTileLoader(this); |
---|
280 | } |
---|
281 | } catch (MalformedURLException e) { |
---|
282 | // ignore, assume that this is not a file |
---|
283 | Logging.log(Logging.LEVEL_DEBUG, e); |
---|
284 | } |
---|
285 | |
---|
286 | if (tileLoader == null) |
---|
287 | tileLoader = new OsmTileLoader(this, headers); |
---|
288 | |
---|
289 | tileCache = new MemoryTileCache(estimateTileCacheSize()); |
---|
290 | } |
---|
291 | |
---|
292 | @Override |
---|
293 | public synchronized void tileLoadingFinished(Tile tile, boolean success) { |
---|
294 | if (tile.hasError()) { |
---|
295 | success = false; |
---|
296 | tile.setImage(null); |
---|
297 | } |
---|
298 | tile.setLoaded(success); |
---|
299 | invalidateLater(); |
---|
300 | Logging.debug("tileLoadingFinished() tile: {0} success: {1}", tile, success); |
---|
301 | } |
---|
302 | |
---|
303 | /** |
---|
304 | * Clears the tile cache. |
---|
305 | */ |
---|
306 | public void clearTileCache() { |
---|
307 | if (tileLoader instanceof CachedTileLoader) { |
---|
308 | ((CachedTileLoader) tileLoader).clearCache(tileSource); |
---|
309 | } |
---|
310 | tileCache.clear(); |
---|
311 | } |
---|
312 | |
---|
313 | /** |
---|
314 | * {@inheritDoc} |
---|
315 | * @deprecated Use {@link TileSourceDisplaySettings#getDx()} |
---|
316 | */ |
---|
317 | @Override |
---|
318 | @Deprecated |
---|
319 | public double getDx() { |
---|
320 | return getDisplaySettings().getDx(); |
---|
321 | } |
---|
322 | |
---|
323 | /** |
---|
324 | * {@inheritDoc} |
---|
325 | * @deprecated Use {@link TileSourceDisplaySettings#getDy()} |
---|
326 | */ |
---|
327 | @Override |
---|
328 | @Deprecated |
---|
329 | public double getDy() { |
---|
330 | return getDisplaySettings().getDy(); |
---|
331 | } |
---|
332 | |
---|
333 | /** |
---|
334 | * {@inheritDoc} |
---|
335 | * @deprecated Use {@link TileSourceDisplaySettings} |
---|
336 | */ |
---|
337 | @Override |
---|
338 | @Deprecated |
---|
339 | public void setOffset(OffsetBookmark offset) { |
---|
340 | getDisplaySettings().setOffsetBookmark(offset); |
---|
341 | } |
---|
342 | |
---|
343 | @Override |
---|
344 | public Object getInfoComponent() { |
---|
345 | JPanel panel = (JPanel) super.getInfoComponent(); |
---|
346 | List<List<String>> content = new ArrayList<>(); |
---|
347 | Collection<String> nativeProjections = getNativeProjections(); |
---|
348 | if (nativeProjections != null) { |
---|
349 | content.add(Arrays.asList(tr("Native projections"), Utils.join(", ", getNativeProjections()))); |
---|
350 | } |
---|
351 | EastNorth offset = getDisplaySettings().getDisplacement(); |
---|
352 | if (offset.distanceSq(0, 0) > 1e-10) { |
---|
353 | content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north())); |
---|
354 | } |
---|
355 | if (coordinateConverter.requiresReprojection()) { |
---|
356 | content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS())); |
---|
357 | content.add(Arrays.asList(tr("Tile display projection"), Main.getProjection().toCode())); |
---|
358 | } |
---|
359 | content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel))); |
---|
360 | for (List<String> entry: content) { |
---|
361 | panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); |
---|
362 | panel.add(GBC.glue(5, 0), GBC.std()); |
---|
363 | panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); |
---|
364 | } |
---|
365 | return panel; |
---|
366 | } |
---|
367 | |
---|
368 | @Override |
---|
369 | protected Action getAdjustAction() { |
---|
370 | return adjustAction; |
---|
371 | } |
---|
372 | |
---|
373 | /** |
---|
374 | * Returns average number of screen pixels per tile pixel for current mapview |
---|
375 | * @param zoom zoom level |
---|
376 | * @return average number of screen pixels per tile pixel |
---|
377 | */ |
---|
378 | public double getScaleFactor(int zoom) { |
---|
379 | if (coordinateConverter != null) { |
---|
380 | return coordinateConverter.getScaleFactor(zoom); |
---|
381 | } else { |
---|
382 | return 1; |
---|
383 | } |
---|
384 | } |
---|
385 | |
---|
386 | public int getBestZoom() { |
---|
387 | double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view |
---|
388 | double result = Math.log(factor)/Math.log(2)/2; |
---|
389 | /* |
---|
390 | * Math.log(factor)/Math.log(2) - gives log base 2 of factor |
---|
391 | * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 |
---|
392 | * |
---|
393 | * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET |
---|
394 | * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET |
---|
395 | * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or |
---|
396 | * maps as a imagery layer |
---|
397 | */ |
---|
398 | int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9); |
---|
399 | int minZoom = getMinZoomLvl(); |
---|
400 | int maxZoom = getMaxZoomLvl(); |
---|
401 | if (minZoom <= maxZoom) { |
---|
402 | intResult = Utils.clamp(intResult, minZoom, maxZoom); |
---|
403 | } else if (intResult > maxZoom) { |
---|
404 | intResult = maxZoom; |
---|
405 | } |
---|
406 | return intResult; |
---|
407 | } |
---|
408 | |
---|
409 | /** |
---|
410 | * Default implementation of {@link org.openstreetmap.josm.gui.layer.Layer.LayerAction#supportLayers(List)}. |
---|
411 | * @param layers layers |
---|
412 | * @return {@code true} is layers contains only a {@code TMSLayer} |
---|
413 | */ |
---|
414 | public static boolean actionSupportLayers(List<Layer> layers) { |
---|
415 | return layers.size() == 1 && layers.get(0) instanceof TMSLayer; |
---|
416 | } |
---|
417 | |
---|
418 | private final class ShowTileInfoAction extends AbstractAction { |
---|
419 | |
---|
420 | private ShowTileInfoAction() { |
---|
421 | super(tr("Show tile info")); |
---|
422 | setEnabled(clickedTileHolder.getTile() != null); |
---|
423 | } |
---|
424 | |
---|
425 | private String getSizeString(int size) { |
---|
426 | return new StringBuilder().append(size).append('x').append(size).toString(); |
---|
427 | } |
---|
428 | |
---|
429 | @Override |
---|
430 | public void actionPerformed(ActionEvent ae) { |
---|
431 | Tile clickedTile = clickedTileHolder.getTile(); |
---|
432 | if (clickedTile != null) { |
---|
433 | ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), tr("OK")); |
---|
434 | JPanel panel = new JPanel(new GridBagLayout()); |
---|
435 | Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile); |
---|
436 | String url = ""; |
---|
437 | try { |
---|
438 | url = clickedTile.getUrl(); |
---|
439 | } catch (IOException e) { |
---|
440 | // silence exceptions |
---|
441 | Logging.trace(e); |
---|
442 | } |
---|
443 | |
---|
444 | List<List<String>> content = new ArrayList<>(); |
---|
445 | content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey())); |
---|
446 | content.add(Arrays.asList(tr("Tile URL"), url)); |
---|
447 | content.add(Arrays.asList(tr("Tile size"), |
---|
448 | getSizeString(clickedTile.getTileSource().getTileSize()))); |
---|
449 | content.add(Arrays.asList(tr("Tile display size"), |
---|
450 | new StringBuilder().append(displaySize.getWidth()) |
---|
451 | .append('x') |
---|
452 | .append(displaySize.getHeight()).toString())); |
---|
453 | if (coordinateConverter.requiresReprojection()) { |
---|
454 | content.add(Arrays.asList(tr("Reprojection"), |
---|
455 | clickedTile.getTileSource().getServerCRS() + |
---|
456 | " -> " + Main.getProjection().toCode())); |
---|
457 | BufferedImage img = clickedTile.getImage(); |
---|
458 | if (img != null) { |
---|
459 | content.add(Arrays.asList(tr("Reprojected tile size"), |
---|
460 | img.getWidth() + "x" + img.getHeight())); |
---|
461 | |
---|
462 | } |
---|
463 | } |
---|
464 | for (List<String> entry: content) { |
---|
465 | panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); |
---|
466 | panel.add(GBC.glue(5, 0), GBC.std()); |
---|
467 | panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); |
---|
468 | } |
---|
469 | |
---|
470 | for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) { |
---|
471 | panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std()); |
---|
472 | panel.add(GBC.glue(5, 0), GBC.std()); |
---|
473 | String value = e.getValue(); |
---|
474 | if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { |
---|
475 | value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value))); |
---|
476 | } |
---|
477 | panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); |
---|
478 | |
---|
479 | } |
---|
480 | ed.setIcon(JOptionPane.INFORMATION_MESSAGE); |
---|
481 | ed.setContent(panel); |
---|
482 | ed.showDialog(); |
---|
483 | } |
---|
484 | } |
---|
485 | } |
---|
486 | |
---|
487 | private final class LoadTileAction extends AbstractAction { |
---|
488 | |
---|
489 | private LoadTileAction() { |
---|
490 | super(tr("Load tile")); |
---|
491 | setEnabled(clickedTileHolder.getTile() != null); |
---|
492 | } |
---|
493 | |
---|
494 | @Override |
---|
495 | public void actionPerformed(ActionEvent ae) { |
---|
496 | Tile clickedTile = clickedTileHolder.getTile(); |
---|
497 | if (clickedTile != null) { |
---|
498 | loadTile(clickedTile, true); |
---|
499 | invalidate(); |
---|
500 | } |
---|
501 | } |
---|
502 | } |
---|
503 | |
---|
504 | private void sendOsmTileRequest(String request) { |
---|
505 | Tile clickedTile = clickedTileHolder.getTile(); |
---|
506 | if (clickedTile != null) { |
---|
507 | try { |
---|
508 | new Notification(HttpClient.create(new URL(clickedTile.getUrl() + '/' + request)) |
---|
509 | .connect().fetchContent()).show(); |
---|
510 | } catch (IOException ex) { |
---|
511 | Logging.error(ex); |
---|
512 | } |
---|
513 | } |
---|
514 | } |
---|
515 | |
---|
516 | private final class GetOsmTileStatusAction extends AbstractAction { |
---|
517 | private GetOsmTileStatusAction() { |
---|
518 | super(tr("Get tile status")); |
---|
519 | setEnabled(clickedTileHolder.getTile() != null); |
---|
520 | } |
---|
521 | |
---|
522 | @Override |
---|
523 | public void actionPerformed(ActionEvent e) { |
---|
524 | sendOsmTileRequest("status"); |
---|
525 | } |
---|
526 | } |
---|
527 | |
---|
528 | private final class MarkOsmTileDirtyAction extends AbstractAction { |
---|
529 | private MarkOsmTileDirtyAction() { |
---|
530 | super(tr("Force tile rendering")); |
---|
531 | setEnabled(clickedTileHolder.getTile() != null); |
---|
532 | } |
---|
533 | |
---|
534 | @Override |
---|
535 | public void actionPerformed(ActionEvent e) { |
---|
536 | sendOsmTileRequest("dirty"); |
---|
537 | } |
---|
538 | } |
---|
539 | |
---|
540 | /** |
---|
541 | * Simple class to keep clickedTile within hookUpMapView |
---|
542 | */ |
---|
543 | private static final class TileHolder { |
---|
544 | private Tile t; |
---|
545 | |
---|
546 | public Tile getTile() { |
---|
547 | return t; |
---|
548 | } |
---|
549 | |
---|
550 | public void setTile(Tile t) { |
---|
551 | this.t = t; |
---|
552 | } |
---|
553 | } |
---|
554 | |
---|
555 | /** |
---|
556 | * Creates popup menu items and binds to mouse actions |
---|
557 | */ |
---|
558 | @Override |
---|
559 | public void hookUpMapView() { |
---|
560 | // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter |
---|
561 | initializeIfRequired(); |
---|
562 | super.hookUpMapView(); |
---|
563 | } |
---|
564 | |
---|
565 | @Override |
---|
566 | public LayerPainter attachToMapView(MapViewEvent event) { |
---|
567 | initializeIfRequired(); |
---|
568 | |
---|
569 | event.getMapView().addMouseListener(adapter); |
---|
570 | MapView.addZoomChangeListener(this); |
---|
571 | |
---|
572 | if (this instanceof NativeScaleLayer) { |
---|
573 | event.getMapView().setNativeScaleLayer((NativeScaleLayer) this); |
---|
574 | } |
---|
575 | |
---|
576 | // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not start loading. |
---|
577 | // FIXME: Check if this is still required. |
---|
578 | event.getMapView().repaint(500); |
---|
579 | |
---|
580 | return super.attachToMapView(event); |
---|
581 | } |
---|
582 | |
---|
583 | private void initializeIfRequired() { |
---|
584 | if (tileSource == null) { |
---|
585 | tileSource = getTileSource(); |
---|
586 | if (tileSource == null) { |
---|
587 | throw new IllegalArgumentException(tr("Failed to create tile source")); |
---|
588 | } |
---|
589 | // check if projection is supported |
---|
590 | projectionChanged(null, Main.getProjection()); |
---|
591 | initTileSource(this.tileSource); |
---|
592 | } |
---|
593 | } |
---|
594 | |
---|
595 | @Override |
---|
596 | protected LayerPainter createMapViewPainter(MapViewEvent event) { |
---|
597 | return new TileSourcePainter(); |
---|
598 | } |
---|
599 | |
---|
600 | /** |
---|
601 | * Tile source layer popup menu. |
---|
602 | */ |
---|
603 | public class TileSourceLayerPopup extends JPopupMenu { |
---|
604 | /** |
---|
605 | * Constructs a new {@code TileSourceLayerPopup}. |
---|
606 | */ |
---|
607 | public TileSourceLayerPopup() { |
---|
608 | for (Action a : getCommonEntries()) { |
---|
609 | if (a instanceof LayerAction) { |
---|
610 | add(((LayerAction) a).createMenuComponent()); |
---|
611 | } else { |
---|
612 | add(new JMenuItem(a)); |
---|
613 | } |
---|
614 | } |
---|
615 | add(new JSeparator()); |
---|
616 | add(new JMenuItem(new LoadTileAction())); |
---|
617 | add(new JMenuItem(new ShowTileInfoAction())); |
---|
618 | if (ExpertToggleAction.isExpert() && tileSource.getBaseUrl().contains(".tile.openstreetmap.org/")) { |
---|
619 | add(new JMenuItem(new GetOsmTileStatusAction())); |
---|
620 | add(new JMenuItem(new MarkOsmTileDirtyAction())); |
---|
621 | } |
---|
622 | } |
---|
623 | } |
---|
624 | |
---|
625 | protected int estimateTileCacheSize() { |
---|
626 | Dimension screenSize = GuiHelper.getMaximumScreenSize(); |
---|
627 | int height = screenSize.height; |
---|
628 | int width = screenSize.width; |
---|
629 | int tileSize = 256; // default tile size |
---|
630 | if (tileSource != null) { |
---|
631 | tileSize = tileSource.getTileSize(); |
---|
632 | } |
---|
633 | // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that |
---|
634 | int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1)); |
---|
635 | // add 10% for tiles from different zoom levels |
---|
636 | int ret = (int) Math.ceil( |
---|
637 | Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible |
---|
638 | * 4); |
---|
639 | Logging.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret); |
---|
640 | return ret; |
---|
641 | } |
---|
642 | |
---|
643 | @Override |
---|
644 | public void displaySettingsChanged(DisplaySettingsChangeEvent e) { |
---|
645 | if (tileSource == null) { |
---|
646 | return; |
---|
647 | } |
---|
648 | switch (e.getChangedSetting()) { |
---|
649 | case TileSourceDisplaySettings.AUTO_ZOOM: |
---|
650 | if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) { |
---|
651 | setZoomLevel(getBestZoom()); |
---|
652 | invalidate(); |
---|
653 | } |
---|
654 | break; |
---|
655 | case TileSourceDisplaySettings.AUTO_LOAD: |
---|
656 | if (getDisplaySettings().isAutoLoad()) { |
---|
657 | invalidate(); |
---|
658 | } |
---|
659 | break; |
---|
660 | default: |
---|
661 | // trigger a redraw just to be sure. |
---|
662 | invalidate(); |
---|
663 | } |
---|
664 | } |
---|
665 | |
---|
666 | /** |
---|
667 | * Checks zoom level against settings |
---|
668 | * @param maxZoomLvl zoom level to check |
---|
669 | * @param ts tile source to crosscheck with |
---|
670 | * @return maximum zoom level, not higher than supported by tilesource nor set by the user |
---|
671 | */ |
---|
672 | public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) { |
---|
673 | if (maxZoomLvl > MAX_ZOOM) { |
---|
674 | maxZoomLvl = MAX_ZOOM; |
---|
675 | } |
---|
676 | if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) { |
---|
677 | maxZoomLvl = PROP_MIN_ZOOM_LVL.get(); |
---|
678 | } |
---|
679 | if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) { |
---|
680 | maxZoomLvl = ts.getMaxZoom(); |
---|
681 | } |
---|
682 | return maxZoomLvl; |
---|
683 | } |
---|
684 | |
---|
685 | /** |
---|
686 | * Checks zoom level against settings |
---|
687 | * @param minZoomLvl zoom level to check |
---|
688 | * @param ts tile source to crosscheck with |
---|
689 | * @return minimum zoom level, not higher than supported by tilesource nor set by the user |
---|
690 | */ |
---|
691 | public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { |
---|
692 | if (minZoomLvl < MIN_ZOOM) { |
---|
693 | minZoomLvl = MIN_ZOOM; |
---|
694 | } |
---|
695 | if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { |
---|
696 | minZoomLvl = getMaxZoomLvl(ts); |
---|
697 | } |
---|
698 | if (ts != null && ts.getMinZoom() > minZoomLvl) { |
---|
699 | minZoomLvl = ts.getMinZoom(); |
---|
700 | } |
---|
701 | return minZoomLvl; |
---|
702 | } |
---|
703 | |
---|
704 | /** |
---|
705 | * @param ts TileSource for which we want to know maximum zoom level |
---|
706 | * @return maximum max zoom level, that will be shown on layer |
---|
707 | */ |
---|
708 | public static int getMaxZoomLvl(TileSource ts) { |
---|
709 | return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts); |
---|
710 | } |
---|
711 | |
---|
712 | /** |
---|
713 | * @param ts TileSource for which we want to know minimum zoom level |
---|
714 | * @return minimum zoom level, that will be shown on layer |
---|
715 | */ |
---|
716 | public static int getMinZoomLvl(TileSource ts) { |
---|
717 | return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); |
---|
718 | } |
---|
719 | |
---|
720 | /** |
---|
721 | * Sets maximum zoom level, that layer will attempt show |
---|
722 | * @param maxZoomLvl maximum zoom level |
---|
723 | */ |
---|
724 | public static void setMaxZoomLvl(int maxZoomLvl) { |
---|
725 | PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null)); |
---|
726 | } |
---|
727 | |
---|
728 | /** |
---|
729 | * Sets minimum zoom level, that layer will attempt show |
---|
730 | * @param minZoomLvl minimum zoom level |
---|
731 | */ |
---|
732 | public static void setMinZoomLvl(int minZoomLvl) { |
---|
733 | PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null)); |
---|
734 | } |
---|
735 | |
---|
736 | /** |
---|
737 | * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all |
---|
738 | * changes to visible map (panning/zooming) |
---|
739 | */ |
---|
740 | @Override |
---|
741 | public void zoomChanged() { |
---|
742 | zoomChanged(true); |
---|
743 | } |
---|
744 | |
---|
745 | private void zoomChanged(boolean invalidate) { |
---|
746 | Logging.debug("zoomChanged(): {0}", currentZoomLevel); |
---|
747 | if (tileLoader instanceof TMSCachedTileLoader) { |
---|
748 | ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); |
---|
749 | } |
---|
750 | if (invalidate) { |
---|
751 | invalidate(); |
---|
752 | } |
---|
753 | } |
---|
754 | |
---|
755 | protected int getMaxZoomLvl() { |
---|
756 | if (info.getMaxZoom() != 0) |
---|
757 | return checkMaxZoomLvl(info.getMaxZoom(), tileSource); |
---|
758 | else |
---|
759 | return getMaxZoomLvl(tileSource); |
---|
760 | } |
---|
761 | |
---|
762 | protected int getMinZoomLvl() { |
---|
763 | if (info.getMinZoom() != 0) |
---|
764 | return checkMinZoomLvl(info.getMinZoom(), tileSource); |
---|
765 | else |
---|
766 | return getMinZoomLvl(tileSource); |
---|
767 | } |
---|
768 | |
---|
769 | /** |
---|
770 | * |
---|
771 | * @return if its allowed to zoom in |
---|
772 | */ |
---|
773 | public boolean zoomIncreaseAllowed() { |
---|
774 | boolean zia = currentZoomLevel < this.getMaxZoomLvl(); |
---|
775 | Logging.debug("zoomIncreaseAllowed(): {0} {1} vs. {2}", zia, currentZoomLevel, this.getMaxZoomLvl()); |
---|
776 | return zia; |
---|
777 | } |
---|
778 | |
---|
779 | /** |
---|
780 | * Zoom in, go closer to map. |
---|
781 | * |
---|
782 | * @return true, if zoom increasing was successful, false otherwise |
---|
783 | */ |
---|
784 | public boolean increaseZoomLevel() { |
---|
785 | if (zoomIncreaseAllowed()) { |
---|
786 | currentZoomLevel++; |
---|
787 | Logging.debug("increasing zoom level to: {0}", currentZoomLevel); |
---|
788 | zoomChanged(); |
---|
789 | } else { |
---|
790 | Logging.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+ |
---|
791 | "Max.zZoom Level "+this.getMaxZoomLvl()+" reached."); |
---|
792 | return false; |
---|
793 | } |
---|
794 | return true; |
---|
795 | } |
---|
796 | |
---|
797 | /** |
---|
798 | * Get the current zoom level of the layer |
---|
799 | * @return the current zoom level |
---|
800 | * @since 12603 |
---|
801 | */ |
---|
802 | public int getZoomLevel() { |
---|
803 | return currentZoomLevel; |
---|
804 | } |
---|
805 | |
---|
806 | /** |
---|
807 | * Sets the zoom level of the layer |
---|
808 | * @param zoom zoom level |
---|
809 | * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels |
---|
810 | */ |
---|
811 | public boolean setZoomLevel(int zoom) { |
---|
812 | return setZoomLevel(zoom, true); |
---|
813 | } |
---|
814 | |
---|
815 | private boolean setZoomLevel(int zoom, boolean invalidate) { |
---|
816 | if (zoom == currentZoomLevel) return true; |
---|
817 | if (zoom > this.getMaxZoomLvl()) return false; |
---|
818 | if (zoom < this.getMinZoomLvl()) return false; |
---|
819 | currentZoomLevel = zoom; |
---|
820 | zoomChanged(invalidate); |
---|
821 | return true; |
---|
822 | } |
---|
823 | |
---|
824 | /** |
---|
825 | * Check if zooming out is allowed |
---|
826 | * |
---|
827 | * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) |
---|
828 | */ |
---|
829 | public boolean zoomDecreaseAllowed() { |
---|
830 | boolean zda = currentZoomLevel > this.getMinZoomLvl(); |
---|
831 | Logging.debug("zoomDecreaseAllowed(): {0} {1} vs. {2}", zda, currentZoomLevel, this.getMinZoomLvl()); |
---|
832 | return zda; |
---|
833 | } |
---|
834 | |
---|
835 | /** |
---|
836 | * Zoom out from map. |
---|
837 | * |
---|
838 | * @return true, if zoom increasing was successfull, false othervise |
---|
839 | */ |
---|
840 | public boolean decreaseZoomLevel() { |
---|
841 | if (zoomDecreaseAllowed()) { |
---|
842 | Logging.debug("decreasing zoom level to: {0}", currentZoomLevel); |
---|
843 | currentZoomLevel--; |
---|
844 | zoomChanged(); |
---|
845 | } else { |
---|
846 | return false; |
---|
847 | } |
---|
848 | return true; |
---|
849 | } |
---|
850 | |
---|
851 | private Tile getOrCreateTile(TilePosition tilePosition) { |
---|
852 | return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); |
---|
853 | } |
---|
854 | |
---|
855 | private Tile getOrCreateTile(int x, int y, int zoom) { |
---|
856 | Tile tile = getTile(x, y, zoom); |
---|
857 | if (tile == null) { |
---|
858 | if (coordinateConverter.requiresReprojection()) { |
---|
859 | tile = new ReprojectionTile(tileSource, x, y, zoom); |
---|
860 | } else { |
---|
861 | tile = new Tile(tileSource, x, y, zoom); |
---|
862 | } |
---|
863 | tileCache.addTile(tile); |
---|
864 | } |
---|
865 | return tile; |
---|
866 | } |
---|
867 | |
---|
868 | private Tile getTile(TilePosition tilePosition) { |
---|
869 | return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); |
---|
870 | } |
---|
871 | |
---|
872 | /** |
---|
873 | * Returns tile at given position. |
---|
874 | * This can and will return null for tiles that are not already in the cache. |
---|
875 | * @param x tile number on the x axis of the tile to be retrieved |
---|
876 | * @param y tile number on the y axis of the tile to be retrieved |
---|
877 | * @param zoom zoom level of the tile to be retrieved |
---|
878 | * @return tile at given position |
---|
879 | */ |
---|
880 | private Tile getTile(int x, int y, int zoom) { |
---|
881 | if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom) |
---|
882 | || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom)) |
---|
883 | return null; |
---|
884 | return tileCache.getTile(tileSource, x, y, zoom); |
---|
885 | } |
---|
886 | |
---|
887 | private boolean loadTile(Tile tile, boolean force) { |
---|
888 | if (tile == null) |
---|
889 | return false; |
---|
890 | if (!force && (tile.isLoaded() || tile.hasError())) |
---|
891 | return false; |
---|
892 | if (tile.isLoading()) |
---|
893 | return false; |
---|
894 | tileLoader.createTileLoaderJob(tile).submit(force); |
---|
895 | return true; |
---|
896 | } |
---|
897 | |
---|
898 | private TileSet getVisibleTileSet() { |
---|
899 | ProjectionBounds bounds = MainApplication.getMap().mapView.getState().getViewArea().getProjectionBounds(); |
---|
900 | return getTileSet(bounds, currentZoomLevel); |
---|
901 | } |
---|
902 | |
---|
903 | /** |
---|
904 | * Load all visible tiles. |
---|
905 | * @param force {@code true} to force loading if auto-load is disabled |
---|
906 | * @since 11950 |
---|
907 | */ |
---|
908 | public void loadAllTiles(boolean force) { |
---|
909 | TileSet ts = getVisibleTileSet(); |
---|
910 | |
---|
911 | // if there is more than 18 tiles on screen in any direction, do not load all tiles! |
---|
912 | if (ts.tooLarge()) { |
---|
913 | Logging.warn("Not downloading all tiles because there is more than 18 tiles on an axis!"); |
---|
914 | return; |
---|
915 | } |
---|
916 | ts.loadAllTiles(force); |
---|
917 | invalidate(); |
---|
918 | } |
---|
919 | |
---|
920 | /** |
---|
921 | * Load all visible tiles in error. |
---|
922 | * @param force {@code true} to force loading if auto-load is disabled |
---|
923 | * @since 11950 |
---|
924 | */ |
---|
925 | public void loadAllErrorTiles(boolean force) { |
---|
926 | TileSet ts = getVisibleTileSet(); |
---|
927 | ts.loadAllErrorTiles(force); |
---|
928 | invalidate(); |
---|
929 | } |
---|
930 | |
---|
931 | @Override |
---|
932 | public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { |
---|
933 | boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; |
---|
934 | Logging.debug("imageUpdate() done: {0} calling repaint", done); |
---|
935 | |
---|
936 | if (done) { |
---|
937 | invalidate(); |
---|
938 | } else { |
---|
939 | invalidateLater(); |
---|
940 | } |
---|
941 | return !done; |
---|
942 | } |
---|
943 | |
---|
944 | /** |
---|
945 | * Invalidate the layer at a time in the future so that the user still sees the interface responsive. |
---|
946 | */ |
---|
947 | private void invalidateLater() { |
---|
948 | GuiHelper.runInEDT(() -> { |
---|
949 | if (!invalidateLaterTimer.isRunning()) { |
---|
950 | invalidateLaterTimer.setRepeats(false); |
---|
951 | invalidateLaterTimer.start(); |
---|
952 | } |
---|
953 | }); |
---|
954 | } |
---|
955 | |
---|
956 | private boolean imageLoaded(Image i) { |
---|
957 | if (i == null) |
---|
958 | return false; |
---|
959 | int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this); |
---|
960 | return (status & ALLBITS) != 0; |
---|
961 | } |
---|
962 | |
---|
963 | /** |
---|
964 | * Returns the image for the given tile image is loaded. |
---|
965 | * Otherwise returns null. |
---|
966 | * |
---|
967 | * @param tile the Tile for which the image should be returned |
---|
968 | * @return the image of the tile or null. |
---|
969 | */ |
---|
970 | private BufferedImage getLoadedTileImage(Tile tile) { |
---|
971 | BufferedImage img = tile.getImage(); |
---|
972 | if (!imageLoaded(img)) |
---|
973 | return null; |
---|
974 | return img; |
---|
975 | } |
---|
976 | |
---|
977 | /** |
---|
978 | * Draw a tile image on screen. |
---|
979 | * @param g the Graphics2D |
---|
980 | * @param toDrawImg tile image |
---|
981 | * @param anchorImage tile anchor in image coordinates |
---|
982 | * @param anchorScreen tile anchor in screen coordinates |
---|
983 | * @param clip clipping region in screen coordinates (can be null) |
---|
984 | */ |
---|
985 | private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) { |
---|
986 | AffineTransform imageToScreen = anchorImage.convert(anchorScreen); |
---|
987 | Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null); |
---|
988 | Point2D screen1 = imageToScreen.transform(new Point.Double( |
---|
989 | toDrawImg.getWidth(), toDrawImg.getHeight()), null); |
---|
990 | |
---|
991 | Shape oldClip = null; |
---|
992 | if (clip != null) { |
---|
993 | oldClip = g.getClip(); |
---|
994 | g.clip(clip); |
---|
995 | } |
---|
996 | g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()), |
---|
997 | (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()), |
---|
998 | (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this); |
---|
999 | if (clip != null) { |
---|
1000 | g.setClip(oldClip); |
---|
1001 | } |
---|
1002 | } |
---|
1003 | |
---|
1004 | private List<Tile> paintTileImages(Graphics2D g, TileSet ts) { |
---|
1005 | Object paintMutex = new Object(); |
---|
1006 | List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>()); |
---|
1007 | ts.visitTiles(tile -> { |
---|
1008 | boolean miss = false; |
---|
1009 | BufferedImage img = null; |
---|
1010 | TileAnchor anchorImage = null; |
---|
1011 | if (!tile.isLoaded() || tile.hasError()) { |
---|
1012 | miss = true; |
---|
1013 | } else { |
---|
1014 | synchronized (tile) { |
---|
1015 | img = getLoadedTileImage(tile); |
---|
1016 | anchorImage = getAnchor(tile, img); |
---|
1017 | } |
---|
1018 | if (img == null || anchorImage == null) { |
---|
1019 | miss = true; |
---|
1020 | } |
---|
1021 | } |
---|
1022 | if (miss) { |
---|
1023 | missed.add(new TilePosition(tile)); |
---|
1024 | return; |
---|
1025 | } |
---|
1026 | |
---|
1027 | img = applyImageProcessors(img); |
---|
1028 | |
---|
1029 | TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); |
---|
1030 | synchronized (paintMutex) { |
---|
1031 | //cannot paint in parallel |
---|
1032 | drawImageInside(g, img, anchorImage, anchorScreen, null); |
---|
1033 | } |
---|
1034 | MapView mapView = MainApplication.getMap().mapView; |
---|
1035 | if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) { |
---|
1036 | // This means we have a reprojected tile in memory cache, but not at |
---|
1037 | // current scale. Generally, the positioning of the tile will still |
---|
1038 | // be correct, but for best image quality, the tile should be |
---|
1039 | // reprojected to the target scale. The original tile image should |
---|
1040 | // still be in disk cache, so this is fairly cheap. |
---|
1041 | ((ReprojectionTile) tile).invalidate(); |
---|
1042 | loadTile(tile, false); |
---|
1043 | } |
---|
1044 | |
---|
1045 | }, missed::add); |
---|
1046 | |
---|
1047 | return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList()); |
---|
1048 | } |
---|
1049 | |
---|
1050 | // This function is called for several zoom levels, not just the current one. |
---|
1051 | // It should not trigger any tiles to be downloaded. |
---|
1052 | // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory. |
---|
1053 | // |
---|
1054 | // The "border" tile tells us the boundaries of where we may drawn. |
---|
1055 | // It will not be from the zoom level that is being drawn currently. |
---|
1056 | // If drawing the displayZoomLevel, border is null and we draw the entire tile set. |
---|
1057 | private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) { |
---|
1058 | if (zoom <= 0) return Collections.emptyList(); |
---|
1059 | Shape borderClip = coordinateConverter.getTileShapeScreen(border); |
---|
1060 | List<Tile> missedTiles = new LinkedList<>(); |
---|
1061 | // The callers of this code *require* that we return any tiles that we do not draw in missedTiles. |
---|
1062 | // ts.allExistingTiles() by default will only return already-existing tiles. |
---|
1063 | // However, we need to return *all* tiles to the callers, so force creation here. |
---|
1064 | for (Tile tile : ts.allTilesCreate()) { |
---|
1065 | boolean miss = false; |
---|
1066 | BufferedImage img = null; |
---|
1067 | TileAnchor anchorImage = null; |
---|
1068 | if (!tile.isLoaded() || tile.hasError()) { |
---|
1069 | miss = true; |
---|
1070 | } else { |
---|
1071 | synchronized (tile) { |
---|
1072 | img = getLoadedTileImage(tile); |
---|
1073 | anchorImage = getAnchor(tile, img); |
---|
1074 | } |
---|
1075 | |
---|
1076 | if (img == null || anchorImage == null) { |
---|
1077 | miss = true; |
---|
1078 | } |
---|
1079 | } |
---|
1080 | if (miss) { |
---|
1081 | missedTiles.add(tile); |
---|
1082 | continue; |
---|
1083 | } |
---|
1084 | |
---|
1085 | // applying all filters to this layer |
---|
1086 | img = applyImageProcessors(img); |
---|
1087 | |
---|
1088 | Shape clip; |
---|
1089 | if (tileSource.isInside(tile, border)) { |
---|
1090 | clip = null; |
---|
1091 | } else if (tileSource.isInside(border, tile)) { |
---|
1092 | clip = borderClip; |
---|
1093 | } else { |
---|
1094 | continue; |
---|
1095 | } |
---|
1096 | TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); |
---|
1097 | drawImageInside(g, img, anchorImage, anchorScreen, clip); |
---|
1098 | } |
---|
1099 | return missedTiles; |
---|
1100 | } |
---|
1101 | |
---|
1102 | private static TileAnchor getAnchor(Tile tile, BufferedImage image) { |
---|
1103 | if (tile instanceof ReprojectionTile) { |
---|
1104 | return ((ReprojectionTile) tile).getAnchor(); |
---|
1105 | } else if (image != null) { |
---|
1106 | return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight())); |
---|
1107 | } else { |
---|
1108 | return null; |
---|
1109 | } |
---|
1110 | } |
---|
1111 | |
---|
1112 | private void myDrawString(Graphics g, String text, int x, int y) { |
---|
1113 | Color oldColor = g.getColor(); |
---|
1114 | String textToDraw = text; |
---|
1115 | if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) { |
---|
1116 | // text longer than tile size, split it |
---|
1117 | StringBuilder line = new StringBuilder(); |
---|
1118 | StringBuilder ret = new StringBuilder(); |
---|
1119 | for (String s: text.split(" ")) { |
---|
1120 | if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) { |
---|
1121 | ret.append(line).append('\n'); |
---|
1122 | line.setLength(0); |
---|
1123 | } |
---|
1124 | line.append(s).append(' '); |
---|
1125 | } |
---|
1126 | ret.append(line); |
---|
1127 | textToDraw = ret.toString(); |
---|
1128 | } |
---|
1129 | int offset = 0; |
---|
1130 | for (String s: textToDraw.split("\n")) { |
---|
1131 | g.setColor(Color.black); |
---|
1132 | g.drawString(s, x + 1, y + offset + 1); |
---|
1133 | g.setColor(oldColor); |
---|
1134 | g.drawString(s, x, y + offset); |
---|
1135 | offset += g.getFontMetrics().getHeight() + 3; |
---|
1136 | } |
---|
1137 | } |
---|
1138 | |
---|
1139 | private void paintTileText(Tile tile, Graphics2D g) { |
---|
1140 | if (tile == null) { |
---|
1141 | return; |
---|
1142 | } |
---|
1143 | Point2D p = coordinateConverter.getPixelForTile(tile); |
---|
1144 | int fontHeight = g.getFontMetrics().getHeight(); |
---|
1145 | int x = (int) p.getX(); |
---|
1146 | int y = (int) p.getY(); |
---|
1147 | int texty = y + 2 + fontHeight; |
---|
1148 | |
---|
1149 | /*if (PROP_DRAW_DEBUG.get()) { |
---|
1150 | myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty); |
---|
1151 | texty += 1 + fontHeight; |
---|
1152 | if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) { |
---|
1153 | myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty); |
---|
1154 | texty += 1 + fontHeight; |
---|
1155 | } |
---|
1156 | } |
---|
1157 | |
---|
1158 | String tileStatus = tile.getStatus(); |
---|
1159 | if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) { |
---|
1160 | myDrawString(g, tr("image " + tileStatus), p.x + 2, texty); |
---|
1161 | texty += 1 + fontHeight; |
---|
1162 | }*/ |
---|
1163 | |
---|
1164 | if (tile.hasError() && getDisplaySettings().isShowErrors()) { |
---|
1165 | myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty); |
---|
1166 | //texty += 1 + fontHeight; |
---|
1167 | } |
---|
1168 | |
---|
1169 | if (Logging.isDebugEnabled()) { |
---|
1170 | // draw tile outline in semi-transparent red |
---|
1171 | g.setColor(new Color(255, 0, 0, 50)); |
---|
1172 | g.draw(coordinateConverter.getTileShapeScreen(tile)); |
---|
1173 | } |
---|
1174 | } |
---|
1175 | |
---|
1176 | private LatLon getShiftedLatLon(EastNorth en) { |
---|
1177 | return coordinateConverter.getProjecting().eastNorth2latlonClamped(en); |
---|
1178 | } |
---|
1179 | |
---|
1180 | private ICoordinate getShiftedCoord(EastNorth en) { |
---|
1181 | return CoordinateConversion.llToCoor(getShiftedLatLon(en)); |
---|
1182 | } |
---|
1183 | |
---|
1184 | private final TileSet nullTileSet = new TileSet(); |
---|
1185 | |
---|
1186 | protected class TileSet extends TileRange { |
---|
1187 | |
---|
1188 | private volatile TileSetInfo info; |
---|
1189 | |
---|
1190 | protected TileSet(TileXY t1, TileXY t2, int zoom) { |
---|
1191 | super(t1, t2, zoom); |
---|
1192 | sanitize(); |
---|
1193 | } |
---|
1194 | |
---|
1195 | protected TileSet(TileRange range) { |
---|
1196 | super(range); |
---|
1197 | sanitize(); |
---|
1198 | } |
---|
1199 | |
---|
1200 | /** |
---|
1201 | * null tile set |
---|
1202 | */ |
---|
1203 | private TileSet() { |
---|
1204 | // default |
---|
1205 | } |
---|
1206 | |
---|
1207 | protected void sanitize() { |
---|
1208 | if (minX < tileSource.getTileXMin(zoom)) { |
---|
1209 | minX = tileSource.getTileXMin(zoom); |
---|
1210 | } |
---|
1211 | if (minY < tileSource.getTileYMin(zoom)) { |
---|
1212 | minY = tileSource.getTileYMin(zoom); |
---|
1213 | } |
---|
1214 | if (maxX > tileSource.getTileXMax(zoom)) { |
---|
1215 | maxX = tileSource.getTileXMax(zoom); |
---|
1216 | } |
---|
1217 | if (maxY > tileSource.getTileYMax(zoom)) { |
---|
1218 | maxY = tileSource.getTileYMax(zoom); |
---|
1219 | } |
---|
1220 | } |
---|
1221 | |
---|
1222 | private boolean tooSmall() { |
---|
1223 | return this.tilesSpanned() < 2.1; |
---|
1224 | } |
---|
1225 | |
---|
1226 | private boolean tooLarge() { |
---|
1227 | return insane() || this.tilesSpanned() > 20; |
---|
1228 | } |
---|
1229 | |
---|
1230 | private boolean insane() { |
---|
1231 | return tileCache == null || size() > tileCache.getCacheSize(); |
---|
1232 | } |
---|
1233 | |
---|
1234 | /** |
---|
1235 | * Get all tiles represented by this TileSet that are already in the tileCache. |
---|
1236 | * @return all tiles represented by this TileSet that are already in the tileCache |
---|
1237 | */ |
---|
1238 | private List<Tile> allExistingTiles() { |
---|
1239 | return allTiles(AbstractTileSourceLayer.this::getTile); |
---|
1240 | } |
---|
1241 | |
---|
1242 | private List<Tile> allTilesCreate() { |
---|
1243 | return allTiles(AbstractTileSourceLayer.this::getOrCreateTile); |
---|
1244 | } |
---|
1245 | |
---|
1246 | private List<Tile> allTiles(Function<TilePosition, Tile> mapper) { |
---|
1247 | return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList()); |
---|
1248 | } |
---|
1249 | |
---|
1250 | /** |
---|
1251 | * Gets a stream of all tile positions in this set |
---|
1252 | * @return A stream of all positions |
---|
1253 | */ |
---|
1254 | public Stream<TilePosition> tilePositions() { |
---|
1255 | if (zoom == 0 || this.insane()) { |
---|
1256 | return Stream.empty(); // Tileset is either empty or too large |
---|
1257 | } else { |
---|
1258 | return IntStream.rangeClosed(minX, maxX).mapToObj( |
---|
1259 | x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom)) |
---|
1260 | ).flatMap(Function.identity()); |
---|
1261 | } |
---|
1262 | } |
---|
1263 | |
---|
1264 | private List<Tile> allLoadedTiles() { |
---|
1265 | return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList()); |
---|
1266 | } |
---|
1267 | |
---|
1268 | /** |
---|
1269 | * @return comparator, that sorts the tiles from the center to the edge of the current screen |
---|
1270 | */ |
---|
1271 | private Comparator<Tile> getTileDistanceComparator() { |
---|
1272 | final int centerX = (int) Math.ceil((minX + maxX) / 2d); |
---|
1273 | final int centerY = (int) Math.ceil((minY + maxY) / 2d); |
---|
1274 | return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY)); |
---|
1275 | } |
---|
1276 | |
---|
1277 | private void loadAllTiles(boolean force) { |
---|
1278 | if (!getDisplaySettings().isAutoLoad() && !force) |
---|
1279 | return; |
---|
1280 | List<Tile> allTiles = allTilesCreate(); |
---|
1281 | allTiles.sort(getTileDistanceComparator()); |
---|
1282 | for (Tile t : allTiles) { |
---|
1283 | loadTile(t, force); |
---|
1284 | } |
---|
1285 | } |
---|
1286 | |
---|
1287 | private void loadAllErrorTiles(boolean force) { |
---|
1288 | if (!getDisplaySettings().isAutoLoad() && !force) |
---|
1289 | return; |
---|
1290 | for (Tile t : this.allTilesCreate()) { |
---|
1291 | if (t.hasError()) { |
---|
1292 | tileLoader.createTileLoaderJob(t).submit(force); |
---|
1293 | } |
---|
1294 | } |
---|
1295 | } |
---|
1296 | |
---|
1297 | /** |
---|
1298 | * Call the given paint method for all tiles in this tile set.<p> |
---|
1299 | * Uses a parallel stream. |
---|
1300 | * @param visitor A visitor to call for each tile. |
---|
1301 | * @param missed a consumer to call for each missed tile. |
---|
1302 | */ |
---|
1303 | public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) { |
---|
1304 | tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed)); |
---|
1305 | } |
---|
1306 | |
---|
1307 | private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) { |
---|
1308 | Tile tile = getTile(tp); |
---|
1309 | if (tile == null) { |
---|
1310 | missed.accept(tp); |
---|
1311 | } else { |
---|
1312 | visitor.accept(tile); |
---|
1313 | } |
---|
1314 | } |
---|
1315 | |
---|
1316 | /** |
---|
1317 | * Check if there is any tile fully loaded without error. |
---|
1318 | * @return true if there is any tile fully loaded without error |
---|
1319 | */ |
---|
1320 | public boolean hasVisibleTiles() { |
---|
1321 | return getTileSetInfo().hasVisibleTiles; |
---|
1322 | } |
---|
1323 | |
---|
1324 | /** |
---|
1325 | * Check if there there is a tile that is overzoomed. |
---|
1326 | * <p> |
---|
1327 | * I.e. the server response for one tile was "there is no tile here". |
---|
1328 | * This usually happens when zoomed in too much. The limit depends on |
---|
1329 | * the region, so at the edge of such a region, some tiles may be |
---|
1330 | * available and some not. |
---|
1331 | * @return true if there there is a tile that is overzoomed |
---|
1332 | */ |
---|
1333 | public boolean hasOverzoomedTiles() { |
---|
1334 | return getTileSetInfo().hasOverzoomedTiles; |
---|
1335 | } |
---|
1336 | |
---|
1337 | /** |
---|
1338 | * Check if there are tiles still loading. |
---|
1339 | * <p> |
---|
1340 | * This is the case if there is a tile not yet in the cache, or in the |
---|
1341 | * cache but marked as loading ({@link Tile#isLoading()}. |
---|
1342 | * @return true if there are tiles still loading |
---|
1343 | */ |
---|
1344 | public boolean hasLoadingTiles() { |
---|
1345 | return getTileSetInfo().hasLoadingTiles; |
---|
1346 | } |
---|
1347 | |
---|
1348 | /** |
---|
1349 | * Check if all tiles in the range are fully loaded. |
---|
1350 | * <p> |
---|
1351 | * A tile is considered to be fully loaded even if the result of loading |
---|
1352 | * the tile was an error. |
---|
1353 | * @return true if all tiles in the range are fully loaded |
---|
1354 | */ |
---|
1355 | public boolean hasAllLoadedTiles() { |
---|
1356 | return getTileSetInfo().hasAllLoadedTiles; |
---|
1357 | } |
---|
1358 | |
---|
1359 | private TileSetInfo getTileSetInfo() { |
---|
1360 | if (info == null) { |
---|
1361 | synchronized (this) { |
---|
1362 | if (info == null) { |
---|
1363 | List<Tile> allTiles = this.allExistingTiles(); |
---|
1364 | info = new TileSetInfo(); |
---|
1365 | info.hasLoadingTiles = allTiles.size() < this.size(); |
---|
1366 | info.hasAllLoadedTiles = true; |
---|
1367 | for (Tile t : allTiles) { |
---|
1368 | if ("no-tile".equals(t.getValue("tile-info"))) { |
---|
1369 | info.hasOverzoomedTiles = true; |
---|
1370 | } |
---|
1371 | if (t.isLoaded()) { |
---|
1372 | if (!t.hasError()) { |
---|
1373 | info.hasVisibleTiles = true; |
---|
1374 | } |
---|
1375 | } else { |
---|
1376 | info.hasAllLoadedTiles = false; |
---|
1377 | if (t.isLoading()) { |
---|
1378 | info.hasLoadingTiles = true; |
---|
1379 | } |
---|
1380 | } |
---|
1381 | } |
---|
1382 | } |
---|
1383 | } |
---|
1384 | } |
---|
1385 | return info; |
---|
1386 | } |
---|
1387 | |
---|
1388 | @Override |
---|
1389 | public String toString() { |
---|
1390 | return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size(); |
---|
1391 | } |
---|
1392 | } |
---|
1393 | |
---|
1394 | /** |
---|
1395 | * Data container to hold information about a {@code TileSet} class. |
---|
1396 | */ |
---|
1397 | private static class TileSetInfo { |
---|
1398 | boolean hasVisibleTiles; |
---|
1399 | boolean hasOverzoomedTiles; |
---|
1400 | boolean hasLoadingTiles; |
---|
1401 | boolean hasAllLoadedTiles; |
---|
1402 | } |
---|
1403 | |
---|
1404 | /** |
---|
1405 | * Create a TileSet by EastNorth bbox taking a layer shift in account |
---|
1406 | * @param bounds the EastNorth bounds |
---|
1407 | * @param zoom zoom level |
---|
1408 | * @return the tile set |
---|
1409 | */ |
---|
1410 | protected TileSet getTileSet(ProjectionBounds bounds, int zoom) { |
---|
1411 | if (zoom == 0) |
---|
1412 | return new TileSet(); |
---|
1413 | TileXY t1, t2; |
---|
1414 | IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin()); |
---|
1415 | IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax()); |
---|
1416 | if (coordinateConverter.requiresReprojection()) { |
---|
1417 | Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS()); |
---|
1418 | ProjectionBounds projBounds = new ProjectionBounds( |
---|
1419 | CoordinateConversion.projToEn(topLeftUnshifted), |
---|
1420 | CoordinateConversion.projToEn(botRightUnshifted)); |
---|
1421 | ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, Main.getProjection()); |
---|
1422 | t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMin()), zoom); |
---|
1423 | t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMax()), zoom); |
---|
1424 | } else { |
---|
1425 | t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom); |
---|
1426 | t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom); |
---|
1427 | } |
---|
1428 | return new TileSet(t1, t2, zoom); |
---|
1429 | } |
---|
1430 | |
---|
1431 | private class DeepTileSet { |
---|
1432 | private final ProjectionBounds bounds; |
---|
1433 | private final int minZoom, maxZoom; |
---|
1434 | private final TileSet[] tileSets; |
---|
1435 | |
---|
1436 | @SuppressWarnings("unchecked") |
---|
1437 | DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) { |
---|
1438 | this.bounds = bounds; |
---|
1439 | this.minZoom = minZoom; |
---|
1440 | this.maxZoom = maxZoom; |
---|
1441 | this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1]; |
---|
1442 | } |
---|
1443 | |
---|
1444 | public TileSet getTileSet(int zoom) { |
---|
1445 | if (zoom < minZoom) |
---|
1446 | return nullTileSet; |
---|
1447 | synchronized (tileSets) { |
---|
1448 | TileSet ts = tileSets[zoom-minZoom]; |
---|
1449 | if (ts == null) { |
---|
1450 | ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom); |
---|
1451 | tileSets[zoom-minZoom] = ts; |
---|
1452 | } |
---|
1453 | return ts; |
---|
1454 | } |
---|
1455 | } |
---|
1456 | } |
---|
1457 | |
---|
1458 | @Override |
---|
1459 | public void paint(Graphics2D g, MapView mv, Bounds bounds) { |
---|
1460 | // old and unused. |
---|
1461 | } |
---|
1462 | |
---|
1463 | private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) { |
---|
1464 | int zoom = currentZoomLevel; |
---|
1465 | if (getDisplaySettings().isAutoZoom()) { |
---|
1466 | zoom = getBestZoom(); |
---|
1467 | } |
---|
1468 | |
---|
1469 | DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom); |
---|
1470 | |
---|
1471 | int displayZoomLevel = zoom; |
---|
1472 | |
---|
1473 | boolean noTilesAtZoom = false; |
---|
1474 | if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) { |
---|
1475 | // Auto-detection of tilesource maxzoom (currently fully works only for Bing) |
---|
1476 | TileSet ts0 = dts.getTileSet(zoom); |
---|
1477 | if (!ts0.hasVisibleTiles() && (!ts0.hasLoadingTiles() || ts0.hasOverzoomedTiles())) { |
---|
1478 | noTilesAtZoom = true; |
---|
1479 | } |
---|
1480 | // Find highest zoom level with at least one visible tile |
---|
1481 | for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) { |
---|
1482 | if (dts.getTileSet(tmpZoom).hasVisibleTiles()) { |
---|
1483 | displayZoomLevel = tmpZoom; |
---|
1484 | break; |
---|
1485 | } |
---|
1486 | } |
---|
1487 | // Do binary search between currentZoomLevel and displayZoomLevel |
---|
1488 | while (zoom > displayZoomLevel && !ts0.hasVisibleTiles() && ts0.hasOverzoomedTiles()) { |
---|
1489 | zoom = (zoom + displayZoomLevel)/2; |
---|
1490 | ts0 = dts.getTileSet(zoom); |
---|
1491 | } |
---|
1492 | |
---|
1493 | setZoomLevel(zoom, false); |
---|
1494 | |
---|
1495 | // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level |
---|
1496 | // to make sure there're really no more zoom levels |
---|
1497 | // loading is done in the next if section |
---|
1498 | if (zoom == displayZoomLevel && !ts0.hasLoadingTiles() && zoom < dts.maxZoom) { |
---|
1499 | zoom++; |
---|
1500 | ts0 = dts.getTileSet(zoom); |
---|
1501 | } |
---|
1502 | // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, |
---|
1503 | // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. |
---|
1504 | // loading is done in the next if section |
---|
1505 | while (zoom > dts.minZoom && ts0.hasOverzoomedTiles() && !ts0.hasLoadingTiles()) { |
---|
1506 | zoom--; |
---|
1507 | ts0 = dts.getTileSet(zoom); |
---|
1508 | } |
---|
1509 | } else if (getDisplaySettings().isAutoZoom()) { |
---|
1510 | setZoomLevel(zoom, false); |
---|
1511 | } |
---|
1512 | TileSet ts = dts.getTileSet(zoom); |
---|
1513 | |
---|
1514 | // Too many tiles... refuse to download |
---|
1515 | if (!ts.tooLarge()) { |
---|
1516 | // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level |
---|
1517 | // on zoom in) |
---|
1518 | ts.loadAllTiles(false); |
---|
1519 | } |
---|
1520 | |
---|
1521 | if (displayZoomLevel != zoom) { |
---|
1522 | ts = dts.getTileSet(displayZoomLevel); |
---|
1523 | if (!dts.getTileSet(displayZoomLevel).hasAllLoadedTiles() && displayZoomLevel < zoom) { |
---|
1524 | // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few, |
---|
1525 | // and should not trash the tile cache |
---|
1526 | // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles |
---|
1527 | ts.loadAllTiles(false); |
---|
1528 | } |
---|
1529 | } |
---|
1530 | |
---|
1531 | g.setColor(Color.DARK_GRAY); |
---|
1532 | |
---|
1533 | List<Tile> missedTiles = this.paintTileImages(g, ts); |
---|
1534 | int[] otherZooms = {1, 2, -1, -2, -3, -4, -5}; |
---|
1535 | for (int zoomOffset : otherZooms) { |
---|
1536 | if (!getDisplaySettings().isAutoZoom()) { |
---|
1537 | break; |
---|
1538 | } |
---|
1539 | int newzoom = displayZoomLevel + zoomOffset; |
---|
1540 | if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) { |
---|
1541 | continue; |
---|
1542 | } |
---|
1543 | if (missedTiles.isEmpty()) { |
---|
1544 | break; |
---|
1545 | } |
---|
1546 | List<Tile> newlyMissedTiles = new LinkedList<>(); |
---|
1547 | for (Tile missed : missedTiles) { |
---|
1548 | if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) { |
---|
1549 | // Don't try to paint from higher zoom levels when tile is overzoomed |
---|
1550 | newlyMissedTiles.add(missed); |
---|
1551 | continue; |
---|
1552 | } |
---|
1553 | TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom)); |
---|
1554 | // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying. |
---|
1555 | if (ts2.allLoadedTiles().isEmpty()) { |
---|
1556 | newlyMissedTiles.add(missed); |
---|
1557 | continue; |
---|
1558 | } |
---|
1559 | if (ts2.tooLarge()) { |
---|
1560 | continue; |
---|
1561 | } |
---|
1562 | newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed)); |
---|
1563 | } |
---|
1564 | missedTiles = newlyMissedTiles; |
---|
1565 | } |
---|
1566 | if (Logging.isDebugEnabled() && !missedTiles.isEmpty()) { |
---|
1567 | Logging.debug("still missed {0} in the end", missedTiles.size()); |
---|
1568 | } |
---|
1569 | g.setColor(Color.red); |
---|
1570 | g.setFont(InfoFont); |
---|
1571 | |
---|
1572 | // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge() |
---|
1573 | for (Tile t : ts.allExistingTiles()) { |
---|
1574 | this.paintTileText(t, g); |
---|
1575 | } |
---|
1576 | |
---|
1577 | EastNorth min = pb.getMin(); |
---|
1578 | EastNorth max = pb.getMax(); |
---|
1579 | attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max), |
---|
1580 | displayZoomLevel, this); |
---|
1581 | |
---|
1582 | g.setColor(Color.lightGray); |
---|
1583 | |
---|
1584 | if (ts.insane()) { |
---|
1585 | myDrawString(g, tr("zoom in to load any tiles"), 120, 120); |
---|
1586 | } else if (ts.tooLarge()) { |
---|
1587 | myDrawString(g, tr("zoom in to load more tiles"), 120, 120); |
---|
1588 | } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) { |
---|
1589 | myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120); |
---|
1590 | } |
---|
1591 | if (noTilesAtZoom) { |
---|
1592 | myDrawString(g, tr("No tiles at this zoom level"), 120, 120); |
---|
1593 | } |
---|
1594 | if (Logging.isDebugEnabled()) { |
---|
1595 | myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140); |
---|
1596 | myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); |
---|
1597 | myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); |
---|
1598 | myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185); |
---|
1599 | myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200); |
---|
1600 | if (tileLoader instanceof TMSCachedTileLoader) { |
---|
1601 | int offset = 200; |
---|
1602 | for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n")) { |
---|
1603 | offset += 15; |
---|
1604 | myDrawString(g, tr("Cache stats: {0}", part), 50, offset); |
---|
1605 | } |
---|
1606 | } |
---|
1607 | } |
---|
1608 | } |
---|
1609 | |
---|
1610 | /** |
---|
1611 | * Returns tile for a pixel position.<p> |
---|
1612 | * This isn't very efficient, but it is only used when the user right-clicks on the map. |
---|
1613 | * @param px pixel X coordinate |
---|
1614 | * @param py pixel Y coordinate |
---|
1615 | * @return Tile at pixel position |
---|
1616 | */ |
---|
1617 | private Tile getTileForPixelpos(int px, int py) { |
---|
1618 | Logging.debug("getTileForPixelpos({0}, {1})", px, py); |
---|
1619 | TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel); |
---|
1620 | return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel); |
---|
1621 | } |
---|
1622 | |
---|
1623 | /** |
---|
1624 | * Class to store a menu action and the class it belongs to. |
---|
1625 | */ |
---|
1626 | private static class MenuAddition { |
---|
1627 | final Action addition; |
---|
1628 | @SuppressWarnings("rawtypes") |
---|
1629 | final Class<? extends AbstractTileSourceLayer> clazz; |
---|
1630 | |
---|
1631 | @SuppressWarnings("rawtypes") |
---|
1632 | MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) { |
---|
1633 | this.addition = addition; |
---|
1634 | this.clazz = clazz; |
---|
1635 | } |
---|
1636 | } |
---|
1637 | |
---|
1638 | /** |
---|
1639 | * Register an additional layer context menu entry. |
---|
1640 | * |
---|
1641 | * @param addition additional menu action |
---|
1642 | * @since 11197 |
---|
1643 | */ |
---|
1644 | public static void registerMenuAddition(Action addition) { |
---|
1645 | menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class)); |
---|
1646 | } |
---|
1647 | |
---|
1648 | /** |
---|
1649 | * Register an additional layer context menu entry for a imagery layer |
---|
1650 | * class. The menu entry is valid for the specified class and subclasses |
---|
1651 | * thereof only. |
---|
1652 | * <p> |
---|
1653 | * Example: |
---|
1654 | * <pre> |
---|
1655 | * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class); |
---|
1656 | * </pre> |
---|
1657 | * |
---|
1658 | * @param addition additional menu action |
---|
1659 | * @param clazz class the menu action is registered for |
---|
1660 | * @since 11197 |
---|
1661 | */ |
---|
1662 | public static void registerMenuAddition(Action addition, |
---|
1663 | Class<? extends AbstractTileSourceLayer<?>> clazz) { |
---|
1664 | menuAdditions.add(new MenuAddition(addition, clazz)); |
---|
1665 | } |
---|
1666 | |
---|
1667 | /** |
---|
1668 | * Prepare list of additional layer context menu entries. The list is |
---|
1669 | * empty if there are no additional menu entries. |
---|
1670 | * |
---|
1671 | * @return list of additional layer context menu entries |
---|
1672 | */ |
---|
1673 | private List<Action> getMenuAdditions() { |
---|
1674 | final LinkedList<Action> menuAdds = new LinkedList<>(); |
---|
1675 | for (MenuAddition menuAdd: menuAdditions) { |
---|
1676 | if (menuAdd.clazz.isInstance(this)) { |
---|
1677 | menuAdds.add(menuAdd.addition); |
---|
1678 | } |
---|
1679 | } |
---|
1680 | if (!menuAdds.isEmpty()) { |
---|
1681 | menuAdds.addFirst(SeparatorLayerAction.INSTANCE); |
---|
1682 | } |
---|
1683 | return menuAdds; |
---|
1684 | } |
---|
1685 | |
---|
1686 | @Override |
---|
1687 | public Action[] getMenuEntries() { |
---|
1688 | ArrayList<Action> actions = new ArrayList<>(); |
---|
1689 | actions.addAll(Arrays.asList(getLayerListEntries())); |
---|
1690 | actions.addAll(Arrays.asList(getCommonEntries())); |
---|
1691 | actions.addAll(getMenuAdditions()); |
---|
1692 | actions.add(SeparatorLayerAction.INSTANCE); |
---|
1693 | actions.add(new LayerListPopup.InfoAction(this)); |
---|
1694 | return actions.toArray(new Action[0]); |
---|
1695 | } |
---|
1696 | |
---|
1697 | /** |
---|
1698 | * Returns the contextual menu entries in layer list dialog. |
---|
1699 | * @return the contextual menu entries in layer list dialog |
---|
1700 | */ |
---|
1701 | public Action[] getLayerListEntries() { |
---|
1702 | return new Action[] { |
---|
1703 | LayerListDialog.getInstance().createActivateLayerAction(this), |
---|
1704 | LayerListDialog.getInstance().createShowHideLayerAction(), |
---|
1705 | LayerListDialog.getInstance().createDeleteLayerAction(), |
---|
1706 | SeparatorLayerAction.INSTANCE, |
---|
1707 | // color, |
---|
1708 | new OffsetAction(), |
---|
1709 | new RenameLayerAction(this.getAssociatedFile(), this), |
---|
1710 | SeparatorLayerAction.INSTANCE |
---|
1711 | }; |
---|
1712 | } |
---|
1713 | |
---|
1714 | /** |
---|
1715 | * Returns the common menu entries. |
---|
1716 | * @return the common menu entries |
---|
1717 | */ |
---|
1718 | public Action[] getCommonEntries() { |
---|
1719 | return new Action[] { |
---|
1720 | new AutoLoadTilesAction(this), |
---|
1721 | new AutoZoomAction(this), |
---|
1722 | new ShowErrorsAction(this), |
---|
1723 | new IncreaseZoomAction(this), |
---|
1724 | new DecreaseZoomAction(this), |
---|
1725 | new ZoomToBestAction(this), |
---|
1726 | new ZoomToNativeLevelAction(this), |
---|
1727 | new FlushTileCacheAction(this), |
---|
1728 | new LoadErroneousTilesAction(this), |
---|
1729 | new LoadAllTilesAction(this) |
---|
1730 | }; |
---|
1731 | } |
---|
1732 | |
---|
1733 | @Override |
---|
1734 | public String getToolTipText() { |
---|
1735 | if (getDisplaySettings().isAutoLoad()) { |
---|
1736 | return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); |
---|
1737 | } else { |
---|
1738 | return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); |
---|
1739 | } |
---|
1740 | } |
---|
1741 | |
---|
1742 | @Override |
---|
1743 | public void visitBoundingBox(BoundingXYVisitor v) { |
---|
1744 | } |
---|
1745 | |
---|
1746 | /** |
---|
1747 | * Task responsible for precaching imagery along the gpx track |
---|
1748 | * |
---|
1749 | */ |
---|
1750 | public class PrecacheTask implements TileLoaderListener { |
---|
1751 | private final ProgressMonitor progressMonitor; |
---|
1752 | private int totalCount; |
---|
1753 | private final AtomicInteger processedCount = new AtomicInteger(0); |
---|
1754 | private final TileLoader tileLoader; |
---|
1755 | |
---|
1756 | /** |
---|
1757 | * @param progressMonitor that will be notified about progess of the task |
---|
1758 | */ |
---|
1759 | public PrecacheTask(ProgressMonitor progressMonitor) { |
---|
1760 | this.progressMonitor = progressMonitor; |
---|
1761 | this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource)); |
---|
1762 | if (this.tileLoader instanceof TMSCachedTileLoader) { |
---|
1763 | ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( |
---|
1764 | TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader")); |
---|
1765 | } |
---|
1766 | } |
---|
1767 | |
---|
1768 | /** |
---|
1769 | * @return true, if all is done |
---|
1770 | */ |
---|
1771 | public boolean isFinished() { |
---|
1772 | return processedCount.get() >= totalCount; |
---|
1773 | } |
---|
1774 | |
---|
1775 | /** |
---|
1776 | * @return total number of tiles to download |
---|
1777 | */ |
---|
1778 | public int getTotalCount() { |
---|
1779 | return totalCount; |
---|
1780 | } |
---|
1781 | |
---|
1782 | /** |
---|
1783 | * cancel the task |
---|
1784 | */ |
---|
1785 | public void cancel() { |
---|
1786 | if (tileLoader instanceof TMSCachedTileLoader) { |
---|
1787 | ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); |
---|
1788 | } |
---|
1789 | } |
---|
1790 | |
---|
1791 | @Override |
---|
1792 | public void tileLoadingFinished(Tile tile, boolean success) { |
---|
1793 | int processed = this.processedCount.incrementAndGet(); |
---|
1794 | if (success) { |
---|
1795 | this.progressMonitor.worked(1); |
---|
1796 | this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount)); |
---|
1797 | } else { |
---|
1798 | Logging.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage()); |
---|
1799 | } |
---|
1800 | } |
---|
1801 | |
---|
1802 | /** |
---|
1803 | * @return tile loader that is used to load the tiles |
---|
1804 | */ |
---|
1805 | public TileLoader getTileLoader() { |
---|
1806 | return tileLoader; |
---|
1807 | } |
---|
1808 | } |
---|
1809 | |
---|
1810 | /** |
---|
1811 | * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download |
---|
1812 | * all of the tiles. Buffer contains at least one tile. |
---|
1813 | * |
---|
1814 | * To prevent accidental clear of the queue, new download executor is created with separate queue |
---|
1815 | * |
---|
1816 | * @param progressMonitor progress monitor for download task |
---|
1817 | * @param points lat/lon coordinates to download |
---|
1818 | * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides |
---|
1819 | * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides |
---|
1820 | * @return precache task representing download task |
---|
1821 | */ |
---|
1822 | public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points, |
---|
1823 | double bufferX, double bufferY) { |
---|
1824 | PrecacheTask precacheTask = new PrecacheTask(progressMonitor); |
---|
1825 | final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>( |
---|
1826 | (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey())); |
---|
1827 | for (LatLon point: points) { |
---|
1828 | TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel); |
---|
1829 | TileXY curTile = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(point), currentZoomLevel); |
---|
1830 | TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel); |
---|
1831 | |
---|
1832 | // take at least one tile of buffer |
---|
1833 | int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex()); |
---|
1834 | int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex()); |
---|
1835 | int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex()); |
---|
1836 | int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex()); |
---|
1837 | |
---|
1838 | for (int x = minX; x <= maxX; x++) { |
---|
1839 | for (int y = minY; y <= maxY; y++) { |
---|
1840 | requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel)); |
---|
1841 | } |
---|
1842 | } |
---|
1843 | } |
---|
1844 | |
---|
1845 | precacheTask.totalCount = requestedTiles.size(); |
---|
1846 | precacheTask.progressMonitor.setTicksCount(requestedTiles.size()); |
---|
1847 | |
---|
1848 | TileLoader loader = precacheTask.getTileLoader(); |
---|
1849 | for (Tile t: requestedTiles) { |
---|
1850 | loader.createTileLoaderJob(t).submit(); |
---|
1851 | } |
---|
1852 | return precacheTask; |
---|
1853 | } |
---|
1854 | |
---|
1855 | @Override |
---|
1856 | public boolean isSavable() { |
---|
1857 | return true; // With WMSLayerExporter |
---|
1858 | } |
---|
1859 | |
---|
1860 | @Override |
---|
1861 | public File createAndOpenSaveFileChooser() { |
---|
1862 | return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); |
---|
1863 | } |
---|
1864 | |
---|
1865 | @Override |
---|
1866 | public synchronized void destroy() { |
---|
1867 | super.destroy(); |
---|
1868 | adjustAction.destroy(); |
---|
1869 | } |
---|
1870 | |
---|
1871 | private class TileSourcePainter extends CompatibilityModeLayerPainter { |
---|
1872 | /** The memory handle that will hold our tile source. */ |
---|
1873 | private MemoryHandle<?> memory; |
---|
1874 | |
---|
1875 | @Override |
---|
1876 | public void paint(MapViewGraphics graphics) { |
---|
1877 | allocateCacheMemory(); |
---|
1878 | if (memory != null) { |
---|
1879 | doPaint(graphics); |
---|
1880 | } |
---|
1881 | } |
---|
1882 | |
---|
1883 | private void doPaint(MapViewGraphics graphics) { |
---|
1884 | drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds()); |
---|
1885 | } |
---|
1886 | |
---|
1887 | private void allocateCacheMemory() { |
---|
1888 | if (memory == null) { |
---|
1889 | MemoryManager manager = MemoryManager.getInstance(); |
---|
1890 | if (manager.isAvailable(getEstimatedCacheSize())) { |
---|
1891 | try { |
---|
1892 | memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new); |
---|
1893 | } catch (NotEnoughMemoryException e) { |
---|
1894 | Logging.warn("Could not allocate tile source memory", e); |
---|
1895 | } |
---|
1896 | } |
---|
1897 | } |
---|
1898 | } |
---|
1899 | |
---|
1900 | protected long getEstimatedCacheSize() { |
---|
1901 | return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); |
---|
1902 | } |
---|
1903 | |
---|
1904 | @Override |
---|
1905 | public void detachFromMapView(MapViewEvent event) { |
---|
1906 | event.getMapView().removeMouseListener(adapter); |
---|
1907 | MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); |
---|
1908 | super.detachFromMapView(event); |
---|
1909 | if (memory != null) { |
---|
1910 | memory.free(); |
---|
1911 | } |
---|
1912 | } |
---|
1913 | } |
---|
1914 | |
---|
1915 | @Override |
---|
1916 | public void projectionChanged(Projection oldValue, Projection newValue) { |
---|
1917 | super.projectionChanged(oldValue, newValue); |
---|
1918 | displaySettings.setOffsetBookmark(displaySettings.getOffsetBookmark()); |
---|
1919 | if (tileCache != null) { |
---|
1920 | tileCache.clear(); |
---|
1921 | } |
---|
1922 | } |
---|
1923 | } |
---|