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

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

see #7427 - clear memory cache on projection change

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