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

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

see #15182 - remove dependence on JMapViewer for package data.coor (only useful for imagery)

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