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

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

see #14524 - make AbstractTileSourceLayer#currentZoomLevel private and use getter instead of public access

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