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

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

see #2212 - simplify DeepTileSet by making TileSetInfo part of TileSet

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