source: josm/trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java@ 11953

Last change on this file since 11953 was 11953, checked in by bastiK, 7 years ago

see #7427 - make sure layer is painted only once after zoom change

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