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

Last change on this file since 13214 was 13214, checked in by Don-vip, 14 months ago

fix #15673 - display tile URL as clickable link in "show tile info" + add expert menu entries "Get tile status" (/status) and "Force tile rendering" (/dirty) for tiles from *.tile.openstreetmap.org

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