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

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

sonar - pmd:UselessQualifiedThis - Useless qualified this usage in the same class

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