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