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