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