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

Last change on this file since 12028 was 12028, checked in by Don-vip, 7 years ago

see #14120 - fix java warnings

  • 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()) - (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) {
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 if (((ReprojectionTile) tile).needsUpdate(Main.map.mapView.getScale())) {
1011 ((ReprojectionTile) tile).invalidate();
1012 loadTile(tile, false);
1013 }
1014 }
1015
1016 }, missed::add);
1017
1018 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1019 }
1020
1021 // This function is called for several zoom levels, not just the current one.
1022 // It should not trigger any tiles to be downloaded.
1023 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1024 //
1025 // The "border" tile tells us the boundaries of where we may drawn.
1026 // It will not be from the zoom level that is being drawn currently.
1027 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1028 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) {
1029 if (zoom <= 0) return Collections.emptyList();
1030 Shape borderClip = coordinateConverter.getTileShapeScreen(border);
1031 List<Tile> missedTiles = new LinkedList<>();
1032 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1033 // ts.allExistingTiles() by default will only return already-existing tiles.
1034 // However, we need to return *all* tiles to the callers, so force creation here.
1035 for (Tile tile : ts.allTilesCreate()) {
1036 boolean miss = false;
1037 BufferedImage img = null;
1038 TileAnchor anchorImage = null;
1039 if (!tile.isLoaded() || tile.hasError()) {
1040 miss = true;
1041 } else {
1042 synchronized (tile) {
1043 img = getLoadedTileImage(tile);
1044 anchorImage = getAnchor(tile, img);
1045 }
1046
1047 if (img == null || anchorImage == null) {
1048 miss = true;
1049 }
1050 }
1051 if (miss) {
1052 missedTiles.add(tile);
1053 continue;
1054 }
1055
1056 // applying all filters to this layer
1057 img = applyImageProcessors(img);
1058
1059 Shape clip;
1060 if (tileSource.isInside(tile, border)) {
1061 clip = null;
1062 } else if (tileSource.isInside(border, tile)) {
1063 clip = borderClip;
1064 } else {
1065 continue;
1066 }
1067 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1068 drawImageInside(g, img, anchorImage, anchorScreen, clip);
1069 }
1070 return missedTiles;
1071 }
1072
1073 private static TileAnchor getAnchor(Tile tile, BufferedImage image) {
1074 if (tile instanceof ReprojectionTile) {
1075 return ((ReprojectionTile) tile).getAnchor();
1076 } else if (image != null) {
1077 return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight()));
1078 } else {
1079 return null;
1080 }
1081 }
1082
1083 private void myDrawString(Graphics g, String text, int x, int y) {
1084 Color oldColor = g.getColor();
1085 String textToDraw = text;
1086 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1087 // text longer than tile size, split it
1088 StringBuilder line = new StringBuilder();
1089 StringBuilder ret = new StringBuilder();
1090 for (String s: text.split(" ")) {
1091 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1092 ret.append(line).append('\n');
1093 line.setLength(0);
1094 }
1095 line.append(s).append(' ');
1096 }
1097 ret.append(line);
1098 textToDraw = ret.toString();
1099 }
1100 int offset = 0;
1101 for (String s: textToDraw.split("\n")) {
1102 g.setColor(Color.black);
1103 g.drawString(s, x + 1, y + offset + 1);
1104 g.setColor(oldColor);
1105 g.drawString(s, x, y + offset);
1106 offset += g.getFontMetrics().getHeight() + 3;
1107 }
1108 }
1109
1110 private void paintTileText(Tile tile, Graphics2D g) {
1111 if (tile == null) {
1112 return;
1113 }
1114 Point2D p = coordinateConverter.getPixelForTile(tile);
1115 int fontHeight = g.getFontMetrics().getHeight();
1116 int x = (int) p.getX();
1117 int y = (int) p.getY();
1118 int texty = y + 2 + fontHeight;
1119
1120 /*if (PROP_DRAW_DEBUG.get()) {
1121 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1122 texty += 1 + fontHeight;
1123 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1124 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1125 texty += 1 + fontHeight;
1126 }
1127 }
1128
1129 String tileStatus = tile.getStatus();
1130 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1131 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1132 texty += 1 + fontHeight;
1133 }*/
1134
1135 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1136 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
1137 //texty += 1 + fontHeight;
1138 }
1139
1140 if (Main.isDebugEnabled()) {
1141 // draw tile outline in semi-transparent red
1142 g.setColor(new Color(255, 0, 0, 50));
1143 g.draw(coordinateConverter.getTileShapeScreen(tile));
1144 }
1145 }
1146
1147 private LatLon getShiftedLatLon(EastNorth en) {
1148 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1149 }
1150
1151 private ICoordinate getShiftedCoord(EastNorth en) {
1152 return getShiftedLatLon(en).toCoordinate();
1153 }
1154
1155 private final TileSet nullTileSet = new TileSet();
1156
1157 protected class TileSet extends TileRange {
1158
1159 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1160 super(t1, t2, zoom);
1161 sanitize();
1162 }
1163
1164 protected TileSet(TileRange range) {
1165 super(range);
1166 sanitize();
1167 }
1168
1169 /**
1170 * null tile set
1171 */
1172 private TileSet() {
1173 // default
1174 }
1175
1176 protected void sanitize() {
1177 if (minX < tileSource.getTileXMin(zoom)) {
1178 minX = tileSource.getTileXMin(zoom);
1179 }
1180 if (minY < tileSource.getTileYMin(zoom)) {
1181 minY = tileSource.getTileYMin(zoom);
1182 }
1183 if (maxX > tileSource.getTileXMax(zoom)) {
1184 maxX = tileSource.getTileXMax(zoom);
1185 }
1186 if (maxY > tileSource.getTileYMax(zoom)) {
1187 maxY = tileSource.getTileYMax(zoom);
1188 }
1189 }
1190
1191 private boolean tooSmall() {
1192 return this.tilesSpanned() < 2.1;
1193 }
1194
1195 private boolean tooLarge() {
1196 return insane() || this.tilesSpanned() > 20;
1197 }
1198
1199 private boolean insane() {
1200 return tileCache == null || size() > tileCache.getCacheSize();
1201 }
1202
1203 /**
1204 * Get all tiles represented by this TileSet that are already in the tileCache.
1205 * @return all tiles represented by this TileSet that are already in the tileCache
1206 */
1207 private List<Tile> allExistingTiles() {
1208 return allTiles(AbstractTileSourceLayer.this::getTile);
1209 }
1210
1211 private List<Tile> allTilesCreate() {
1212 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1213 }
1214
1215 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1216 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1217 }
1218
1219 /**
1220 * Gets a stream of all tile positions in this set
1221 * @return A stream of all positions
1222 */
1223 public Stream<TilePosition> tilePositions() {
1224 if (zoom == 0 || this.insane()) {
1225 return Stream.empty(); // Tileset is either empty or too large
1226 } else {
1227 return IntStream.rangeClosed(minX, maxX).mapToObj(
1228 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
1229 ).flatMap(Function.identity());
1230 }
1231 }
1232
1233 private List<Tile> allLoadedTiles() {
1234 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1235 }
1236
1237 /**
1238 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1239 */
1240 private Comparator<Tile> getTileDistanceComparator() {
1241 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1242 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1243 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1244 }
1245
1246 private void loadAllTiles(boolean force) {
1247 if (!getDisplaySettings().isAutoLoad() && !force)
1248 return;
1249 List<Tile> allTiles = allTilesCreate();
1250 allTiles.sort(getTileDistanceComparator());
1251 for (Tile t : allTiles) {
1252 loadTile(t, force);
1253 }
1254 }
1255
1256 private void loadAllErrorTiles(boolean force) {
1257 if (!getDisplaySettings().isAutoLoad() && !force)
1258 return;
1259 for (Tile t : this.allTilesCreate()) {
1260 if (t.hasError()) {
1261 tileLoader.createTileLoaderJob(t).submit(force);
1262 }
1263 }
1264 }
1265
1266 /**
1267 * Call the given paint method for all tiles in this tile set.<p>
1268 * Uses a parallel stream.
1269 * @param visitor A visitor to call for each tile.
1270 * @param missed a consumer to call for each missed tile.
1271 */
1272 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1273 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1274 }
1275
1276 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1277 Tile tile = getTile(tp);
1278 if (tile == null) {
1279 missed.accept(tp);
1280 } else {
1281 visitor.accept(tile);
1282 }
1283 }
1284
1285 @Override
1286 public String toString() {
1287 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1288 }
1289 }
1290
1291 /**
1292 * Create a TileSet by EastNorth bbox taking a layer shift in account
1293 * @param bounds the EastNorth bounds
1294 * @param zoom zoom level
1295 * @return the tile set
1296 */
1297 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
1298 if (zoom == 0)
1299 return new TileSet();
1300 TileXY t1, t2;
1301 if (coordinateConverter.requiresReprojection()) {
1302 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
1303 ProjectionBounds projBounds = new ProjectionBounds(
1304 new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMin())),
1305 new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMax())));
1306 ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, Main.getProjection());
1307 t1 = tileSource.projectedToTileXY(bbox.getMin().toProjected(), zoom);
1308 t2 = tileSource.projectedToTileXY(bbox.getMax().toProjected(), zoom);
1309 } else {
1310 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
1311 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
1312 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
1313 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
1314 }
1315 return new TileSet(t1, t2, zoom);
1316 }
1317
1318 private static class TileSetInfo {
1319 boolean hasVisibleTiles;
1320 boolean hasOverzoomedTiles;
1321 boolean hasLoadingTiles;
1322 boolean hasAllLoadedTiles;
1323 }
1324
1325 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
1326 List<Tile> allTiles = ts.allExistingTiles();
1327 TileSetInfo result = new TileSetInfo();
1328 result.hasLoadingTiles = allTiles.size() < ts.size();
1329 result.hasAllLoadedTiles = true;
1330 for (Tile t : allTiles) {
1331 if ("no-tile".equals(t.getValue("tile-info"))) {
1332 result.hasOverzoomedTiles = true;
1333 }
1334 if (t.isLoaded()) {
1335 if (!t.hasError()) {
1336 result.hasVisibleTiles = true;
1337 }
1338 } else {
1339 result.hasAllLoadedTiles = false;
1340 if (t.isLoading()) {
1341 result.hasLoadingTiles = true;
1342 }
1343 }
1344 }
1345 return result;
1346 }
1347
1348 private class DeepTileSet {
1349 private final ProjectionBounds bounds;
1350 private final int minZoom, maxZoom;
1351 private final TileSet[] tileSets;
1352 private final TileSetInfo[] tileSetInfos;
1353
1354 @SuppressWarnings("unchecked")
1355 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1356 this.bounds = bounds;
1357 this.minZoom = minZoom;
1358 this.maxZoom = maxZoom;
1359 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1360 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1361 }
1362
1363 public TileSet getTileSet(int zoom) {
1364 if (zoom < minZoom)
1365 return nullTileSet;
1366 synchronized (tileSets) {
1367 TileSet ts = tileSets[zoom-minZoom];
1368 if (ts == null) {
1369 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom);
1370 tileSets[zoom-minZoom] = ts;
1371 }
1372 return ts;
1373 }
1374 }
1375
1376 public TileSetInfo getTileSetInfo(int zoom) {
1377 if (zoom < minZoom)
1378 return new TileSetInfo();
1379 synchronized (tileSetInfos) {
1380 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1381 if (tsi == null) {
1382 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1383 tileSetInfos[zoom-minZoom] = tsi;
1384 }
1385 return tsi;
1386 }
1387 }
1388 }
1389
1390 @Override
1391 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1392 // old and unused.
1393 }
1394
1395 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1396 int zoom = currentZoomLevel;
1397 if (getDisplaySettings().isAutoZoom()) {
1398 zoom = getBestZoom();
1399 }
1400
1401 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1402 TileSet ts = dts.getTileSet(zoom);
1403
1404 int displayZoomLevel = zoom;
1405
1406 boolean noTilesAtZoom = false;
1407 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1408 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1409 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1410 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1411 noTilesAtZoom = true;
1412 }
1413 // Find highest zoom level with at least one visible tile
1414 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1415 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1416 displayZoomLevel = tmpZoom;
1417 break;
1418 }
1419 }
1420 // Do binary search between currentZoomLevel and displayZoomLevel
1421 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1422 zoom = (zoom + displayZoomLevel)/2;
1423 tsi = dts.getTileSetInfo(zoom);
1424 }
1425
1426 setZoomLevel(zoom, false);
1427
1428 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1429 // to make sure there're really no more zoom levels
1430 // loading is done in the next if section
1431 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1432 zoom++;
1433 tsi = dts.getTileSetInfo(zoom);
1434 }
1435 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1436 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1437 // loading is done in the next if section
1438 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1439 zoom--;
1440 tsi = dts.getTileSetInfo(zoom);
1441 }
1442 ts = dts.getTileSet(zoom);
1443 } else if (getDisplaySettings().isAutoZoom()) {
1444 setZoomLevel(zoom, false);
1445 }
1446
1447 // Too many tiles... refuse to download
1448 if (!ts.tooLarge()) {
1449 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level
1450 // on zoom in)
1451 ts.loadAllTiles(false);
1452 }
1453
1454 if (displayZoomLevel != zoom) {
1455 ts = dts.getTileSet(displayZoomLevel);
1456 if (!dts.getTileSetInfo(displayZoomLevel).hasAllLoadedTiles && displayZoomLevel < zoom) {
1457 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few,
1458 // and should not trash the tile cache
1459 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles
1460 ts.loadAllTiles(false);
1461 }
1462 }
1463
1464 g.setColor(Color.DARK_GRAY);
1465
1466 List<Tile> missedTiles = this.paintTileImages(g, ts);
1467 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5};
1468 for (int zoomOffset : otherZooms) {
1469 if (!getDisplaySettings().isAutoZoom()) {
1470 break;
1471 }
1472 int newzoom = displayZoomLevel + zoomOffset;
1473 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1474 continue;
1475 }
1476 if (missedTiles.isEmpty()) {
1477 break;
1478 }
1479 List<Tile> newlyMissedTiles = new LinkedList<>();
1480 for (Tile missed : missedTiles) {
1481 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) {
1482 // Don't try to paint from higher zoom levels when tile is overzoomed
1483 newlyMissedTiles.add(missed);
1484 continue;
1485 }
1486 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom));
1487 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying.
1488 if (ts2.allLoadedTiles().isEmpty()) {
1489 newlyMissedTiles.add(missed);
1490 continue;
1491 }
1492 if (ts2.tooLarge()) {
1493 continue;
1494 }
1495 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1496 }
1497 missedTiles = newlyMissedTiles;
1498 }
1499 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1500 Main.debug("still missed "+missedTiles.size()+" in the end");
1501 }
1502 g.setColor(Color.red);
1503 g.setFont(InfoFont);
1504
1505 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1506 for (Tile t : ts.allExistingTiles()) {
1507 this.paintTileText(t, g);
1508 }
1509
1510 EastNorth min = pb.getMin();
1511 EastNorth max = pb.getMax();
1512 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
1513 displayZoomLevel, this);
1514
1515 g.setColor(Color.lightGray);
1516
1517 if (ts.insane()) {
1518 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1519 } else if (ts.tooLarge()) {
1520 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1521 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
1522 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
1523 }
1524 if (noTilesAtZoom) {
1525 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1526 }
1527 if (Main.isDebugEnabled()) {
1528 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1529 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1530 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1531 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1532 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1533 if (tileLoader instanceof TMSCachedTileLoader) {
1534 int offset = 200;
1535 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n")) {
1536 offset += 15;
1537 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
1538 }
1539 }
1540 }
1541 }
1542
1543 /**
1544 * Returns tile for a pixel position.<p>
1545 * This isn't very efficient, but it is only used when the user right-clicks on the map.
1546 * @param px pixel X coordinate
1547 * @param py pixel Y coordinate
1548 * @return Tile at pixel position
1549 */
1550 private Tile getTileForPixelpos(int px, int py) {
1551 if (Main.isDebugEnabled()) {
1552 Main.debug("getTileForPixelpos("+px+", "+py+')');
1553 }
1554 TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel);
1555 return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel);
1556 }
1557
1558 /**
1559 * Class to store a menu action and the class it belongs to.
1560 */
1561 private static class MenuAddition {
1562 final Action addition;
1563 @SuppressWarnings("rawtypes")
1564 final Class<? extends AbstractTileSourceLayer> clazz;
1565
1566 @SuppressWarnings("rawtypes")
1567 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) {
1568 this.addition = addition;
1569 this.clazz = clazz;
1570 }
1571 }
1572
1573 /**
1574 * Register an additional layer context menu entry.
1575 *
1576 * @param addition additional menu action
1577 * @since 11197
1578 */
1579 public static void registerMenuAddition(Action addition) {
1580 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class));
1581 }
1582
1583 /**
1584 * Register an additional layer context menu entry for a imagery layer
1585 * class. The menu entry is valid for the specified class and subclasses
1586 * thereof only.
1587 * <p>
1588 * Example:
1589 * <pre>
1590 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class);
1591 * </pre>
1592 *
1593 * @param addition additional menu action
1594 * @param clazz class the menu action is registered for
1595 * @since 11197
1596 */
1597 public static void registerMenuAddition(Action addition,
1598 Class<? extends AbstractTileSourceLayer<?>> clazz) {
1599 menuAdditions.add(new MenuAddition(addition, clazz));
1600 }
1601
1602 /**
1603 * Prepare list of additional layer context menu entries. The list is
1604 * empty if there are no additional menu entries.
1605 *
1606 * @return list of additional layer context menu entries
1607 */
1608 private List<Action> getMenuAdditions() {
1609 final LinkedList<Action> menuAdds = new LinkedList<>();
1610 for (MenuAddition menuAdd: menuAdditions) {
1611 if (menuAdd.clazz.isInstance(this)) {
1612 menuAdds.add(menuAdd.addition);
1613 }
1614 }
1615 if (!menuAdds.isEmpty()) {
1616 menuAdds.addFirst(SeparatorLayerAction.INSTANCE);
1617 }
1618 return menuAdds;
1619 }
1620
1621 @Override
1622 public Action[] getMenuEntries() {
1623 ArrayList<Action> actions = new ArrayList<>();
1624 actions.addAll(Arrays.asList(getLayerListEntries()));
1625 actions.addAll(Arrays.asList(getCommonEntries()));
1626 actions.addAll(getMenuAdditions());
1627 actions.add(SeparatorLayerAction.INSTANCE);
1628 actions.add(new LayerListPopup.InfoAction(this));
1629 return actions.toArray(new Action[actions.size()]);
1630 }
1631
1632 /**
1633 * Returns the contextual menu entries in layer list dialog.
1634 * @return the contextual menu entries in layer list dialog
1635 */
1636 public Action[] getLayerListEntries() {
1637 return new Action[] {
1638 LayerListDialog.getInstance().createActivateLayerAction(this),
1639 LayerListDialog.getInstance().createShowHideLayerAction(),
1640 LayerListDialog.getInstance().createDeleteLayerAction(),
1641 SeparatorLayerAction.INSTANCE,
1642 // color,
1643 new OffsetAction(),
1644 new RenameLayerAction(this.getAssociatedFile(), this),
1645 SeparatorLayerAction.INSTANCE
1646 };
1647 }
1648
1649 /**
1650 * Returns the common menu entries.
1651 * @return the common menu entries
1652 */
1653 public Action[] getCommonEntries() {
1654 return new Action[] {
1655 new AutoLoadTilesAction(this),
1656 new AutoZoomAction(this),
1657 new ShowErrorsAction(this),
1658 new IncreaseZoomAction(this),
1659 new DecreaseZoomAction(this),
1660 new ZoomToBestAction(this),
1661 new ZoomToNativeLevelAction(this),
1662 new FlushTileCacheAction(this),
1663 new LoadErroneousTilesAction(this),
1664 new LoadAllTilesAction(this)
1665 };
1666 }
1667
1668 @Override
1669 public String getToolTipText() {
1670 if (getDisplaySettings().isAutoLoad()) {
1671 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1672 } else {
1673 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1674 }
1675 }
1676
1677 @Override
1678 public void visitBoundingBox(BoundingXYVisitor v) {
1679 }
1680
1681 @Override
1682 public boolean isChanged() {
1683 return false; // we use #invalidate()
1684 }
1685
1686 /**
1687 * Task responsible for precaching imagery along the gpx track
1688 *
1689 */
1690 public class PrecacheTask implements TileLoaderListener {
1691 private final ProgressMonitor progressMonitor;
1692 private int totalCount;
1693 private final AtomicInteger processedCount = new AtomicInteger(0);
1694 private final TileLoader tileLoader;
1695
1696 /**
1697 * @param progressMonitor that will be notified about progess of the task
1698 */
1699 public PrecacheTask(ProgressMonitor progressMonitor) {
1700 this.progressMonitor = progressMonitor;
1701 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1702 if (this.tileLoader instanceof TMSCachedTileLoader) {
1703 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1704 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1705 }
1706 }
1707
1708 /**
1709 * @return true, if all is done
1710 */
1711 public boolean isFinished() {
1712 return processedCount.get() >= totalCount;
1713 }
1714
1715 /**
1716 * @return total number of tiles to download
1717 */
1718 public int getTotalCount() {
1719 return totalCount;
1720 }
1721
1722 /**
1723 * cancel the task
1724 */
1725 public void cancel() {
1726 if (tileLoader instanceof TMSCachedTileLoader) {
1727 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1728 }
1729 }
1730
1731 @Override
1732 public void tileLoadingFinished(Tile tile, boolean success) {
1733 int processed = this.processedCount.incrementAndGet();
1734 if (success) {
1735 this.progressMonitor.worked(1);
1736 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1737 } else {
1738 Main.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage());
1739 }
1740 }
1741
1742 /**
1743 * @return tile loader that is used to load the tiles
1744 */
1745 public TileLoader getTileLoader() {
1746 return tileLoader;
1747 }
1748 }
1749
1750 /**
1751 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1752 * all of the tiles. Buffer contains at least one tile.
1753 *
1754 * To prevent accidental clear of the queue, new download executor is created with separate queue
1755 *
1756 * @param progressMonitor progress monitor for download task
1757 * @param points lat/lon coordinates to download
1758 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1759 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1760 * @return precache task representing download task
1761 */
1762 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,
1763 double bufferX, double bufferY) {
1764 PrecacheTask precacheTask = new PrecacheTask(progressMonitor);
1765 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(
1766 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
1767 for (LatLon point: points) {
1768 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1769 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1770 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1771
1772 // take at least one tile of buffer
1773 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1774 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1775 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1776 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex());
1777
1778 for (int x = minX; x <= maxX; x++) {
1779 for (int y = minY; y <= maxY; y++) {
1780 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1781 }
1782 }
1783 }
1784
1785 precacheTask.totalCount = requestedTiles.size();
1786 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1787
1788 TileLoader loader = precacheTask.getTileLoader();
1789 for (Tile t: requestedTiles) {
1790 loader.createTileLoaderJob(t).submit();
1791 }
1792 return precacheTask;
1793 }
1794
1795 @Override
1796 public boolean isSavable() {
1797 return true; // With WMSLayerExporter
1798 }
1799
1800 @Override
1801 public File createAndOpenSaveFileChooser() {
1802 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1803 }
1804
1805 @Override
1806 public synchronized void destroy() {
1807 super.destroy();
1808 adjustAction.destroy();
1809 }
1810
1811 private class TileSourcePainter extends CompatibilityModeLayerPainter {
1812 /** The memory handle that will hold our tile source. */
1813 private MemoryHandle<?> memory;
1814
1815 @Override
1816 public void paint(MapViewGraphics graphics) {
1817 allocateCacheMemory();
1818 if (memory != null) {
1819 doPaint(graphics);
1820 }
1821 }
1822
1823 private void doPaint(MapViewGraphics graphics) {
1824 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds());
1825 }
1826
1827 private void allocateCacheMemory() {
1828 if (memory == null) {
1829 MemoryManager manager = MemoryManager.getInstance();
1830 if (manager.isAvailable(getEstimatedCacheSize())) {
1831 try {
1832 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
1833 } catch (NotEnoughMemoryException e) {
1834 Main.warn("Could not allocate tile source memory", e);
1835 }
1836 }
1837 }
1838 }
1839
1840 protected long getEstimatedCacheSize() {
1841 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
1842 }
1843
1844 @Override
1845 public void detachFromMapView(MapViewEvent event) {
1846 event.getMapView().removeMouseListener(adapter);
1847 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
1848 super.detachFromMapView(event);
1849 if (memory != null) {
1850 memory.free();
1851 }
1852 }
1853 }
1854
1855 @Override
1856 public void projectionChanged(Projection oldValue, Projection newValue) {
1857 super.projectionChanged(oldValue, newValue);
1858 if (tileCache != null) {
1859 tileCache.clear();
1860 }
1861 }
1862}
Note: See TracBrowser for help on using the repository browser.