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

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

fixed #14734 - Handling imagery offsets when reprojecting

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