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

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

see #11390, fix #13120 - Use a new memory manager for imagery layers (patch by michael2402) - gsoc-core - requires java 8

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