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

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

see #7427 - better return type

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