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