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

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

extract actions from AbstractTileSourceLayer to gui.layer.imagery package

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