Ticket #13386: patch-mapview-extract-imagery-rendering.patch

File patch-mapview-extract-imagery-rendering.patch, 175.1 KB (added by michael2402, 10 years ago)
  • src/org/openstreetmap/josm/data/Bounds.java

    diff --git a/src/org/openstreetmap/josm/data/Bounds.java b/src/org/openstreetmap/josm/data/Bounds.java
    index 5cdbd0e..8108239 100644
    a b public class Bounds {  
    396396    }
    397397
    398398    /**
     399     * Compute the intersection of this with an other bounds object.
     400     * @param other The other bounds
     401     * @return The intersection area or <code>null</code> if they do not intersect.
     402     */
     403    public Bounds intersect(Bounds other) {
     404        if (crosses180thMeridian() || other.crosses180thMeridian()) {
     405            throw new UnsupportedOperationException();
     406        } else {
     407            Bounds bounds = new Bounds(
     408                    Math.max(minLat, other.minLat),
     409                    Math.max(minLon, other.minLon),
     410                    Math.min(maxLat, other.maxLat),
     411                    Math.min(maxLon, other.maxLon), false);
     412            if  (bounds.minLat >= bounds.maxLat || bounds.minLon >= bounds.maxLon) {
     413                return null;
     414            } else {
     415                return bounds;
     416            }
     417        }
     418    }
     419
     420    /**
    399421     * Converts the lat/lon bounding box to an object of type Rectangle2D.Double
    400422     * @return the bounding box to Rectangle2D.Double
    401423     */
  • src/org/openstreetmap/josm/gui/MapViewState.java

    diff --git a/src/org/openstreetmap/josm/gui/MapViewState.java b/src/org/openstreetmap/josm/gui/MapViewState.java
    index 6466eef..ebe4f44 100644
    a b package org.openstreetmap.josm.gui;  
    44import java.awt.Container;
    55import java.awt.Point;
    66import java.awt.Rectangle;
     7import java.awt.Shape;
    78import java.awt.geom.AffineTransform;
    89import java.awt.geom.Area;
    910import java.awt.geom.Path2D;
    import org.openstreetmap.josm.data.coor.EastNorth;  
    2021import org.openstreetmap.josm.data.coor.LatLon;
    2122import org.openstreetmap.josm.data.projection.Projecting;
    2223import org.openstreetmap.josm.data.projection.Projection;
     24import org.openstreetmap.josm.data.projection.ShiftedProjecting;
    2325import org.openstreetmap.josm.gui.download.DownloadDialog;
    2426import org.openstreetmap.josm.tools.bugreport.BugReport;
    2527
    public final class MapViewState {  
    138140    }
    139141
    140142    /**
     143     * Gets the MapViewPoint representation for a position in view coordinates.
     144     * @param point The point in view space.
     145     * @return The MapViewPoint.
     146     */
     147    public MapViewPoint getForView(Point2D point) {
     148        return new MapViewViewPoint(point.getX(), point.getY());
     149    }
     150
     151    /**
    141152     * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate.
    142153     * @param eastNorth the position.
    143154     * @return The point for that position.
    public final class MapViewState {  
    217228    }
    218229
    219230    public Area getArea(Bounds bounds) {
     231        Path2D area = getPath(bounds);
     232        return new Area(area);
     233    }
     234
     235    private Path2D getPath(Bounds bounds) {
    220236        Path2D area = new Path2D.Double();
    221237        bounds.visitEdge(getProjection(), latlon -> {
    222238            MapViewPoint point = getPointFor(latlon);
    public final class MapViewState {  
    227243            }
    228244        });
    229245        area.closePath();
    230         return new Area(area);
     246        return area;
    231247    }
    232248
    233249    /**
    public final class MapViewState {  
    293309    }
    294310
    295311    /**
     312     * Creates a shifted mapviewstate.
     313     * @param offset The delta to apply in east/north space
     314     * @return The shifted MapViewState
     315     */
     316    public MapViewState shifted(EastNorth offset) {
     317        return new MapViewState(new ShiftedProjecting(projecting, offset), this);
     318    }
     319
     320    /**
    296321     * Create the default {@link MapViewState} object for the given map view. The screen position won't be set so that this method can be used
    297322     * before the view was added to the hirarchy.
    298323     * @param width The view width
    public final class MapViewState {  
    390415        }
    391416
    392417        /**
     418         * Gets a rectangle from this point to an other point in lat/lon space, clamped to the world bounds.
     419         * @param p2 The other point
     420         * @return The rectangle
     421         */
     422        public MapViewLatLonRectangle latLonRectTo(MapViewPoint p2) {
     423            return new MapViewLatLonRectangle(getLatLonClamped(), p2.getLatLonClamped());
     424        }
     425
     426        /**
    393427         * Add the given offset to this point
    394428         * @param en The offset in east/north space.
    395429         * @return The new point
    public final class MapViewState {  
    455489    }
    456490
    457491    /**
     492     * This is a shape on the map view area.
     493     * @author Michael Zangl
     494     * @since xxx
     495     */
     496    public interface MapViewArea {
     497
     498        /**
     499         * Gets the shape in view space.
     500         * @return The area in view coordinates
     501         */
     502        public Shape getInView();
     503
     504        /**
     505         * Gets the real bounds that enclose this rectangle.
     506         * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
     507         * @return The bounds.
     508         * @since 10458
     509         */
     510        public Bounds getLatLonBoundsBox();
     511
     512        /**
     513         * Gets the projection bounds for this rectangle.
     514         * @return The projection bounds.
     515         */
     516        public ProjectionBounds getProjectionBounds();
     517
     518        /**
     519         * Check if the given point is contained in this rectangle.
     520         * @param point The position
     521         * @return true if the point is contained in this shape.
     522         */
     523        public boolean contains(MapViewPoint point);
     524    }
     525
     526    /**
    458527     * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
    459528     * @author Michael Zangl
    460529     */
    461     public class MapViewRectangle {
     530    public class MapViewRectangle implements MapViewArea {
    462531        private final MapViewPoint p1;
    463532        private final MapViewPoint p2;
    464533
    public final class MapViewState {  
    472541            this.p2 = p2;
    473542        }
    474543
    475         /**
    476          * Gets the projection bounds for this rectangle.
    477          * @return The projection bounds.
    478          */
     544        @Override
    479545        public ProjectionBounds getProjectionBounds() {
    480546            ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
    481547            b.extend(p2.getEastNorth());
    public final class MapViewState {  
    493559            return b;
    494560        }
    495561
    496         /**
    497          * Gets the real bounds that enclose this rectangle.
    498          * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
    499          * @return The bounds.
    500          * @since 10458
    501          */
     562        @Override
    502563        public Bounds getLatLonBoundsBox() {
    503564            // TODO @michael2402: Use hillclimb.
    504565            return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
    public final class MapViewState {  
    509570         * @return The rectangle.
    510571         * @since 10651
    511572         */
     573        @Override
    512574        public Rectangle2D getInView() {
    513575            double x1 = p1.getInViewX();
    514576            double y1 = p1.getInViewY();
    public final class MapViewState {  
    516578            double y2 = p2.getInViewY();
    517579            return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2));
    518580        }
     581
     582        @Override
     583        public boolean contains(MapViewPoint point) {
     584            return getInView().contains(point.getInView());
     585        }
    519586    }
    520587
     588    /**
     589     * A rectangle in lat/lon space
     590     * @author Michael Zangl
     591     * @since xxx
     592     */
     593    public class MapViewLatLonRectangle implements MapViewArea {
     594
     595        private final Bounds bounds;
     596
     597        MapViewLatLonRectangle(LatLon l1, LatLon l2) {
     598            bounds = new Bounds(l1);
     599            bounds.extend(l2);
     600        }
     601
     602        @Override
     603        public Shape getInView() {
     604            return getPath(bounds);
     605        }
     606
     607        @Override
     608        public boolean contains(MapViewPoint point) {
     609            return bounds.contains(point.getLatLon());
     610        }
     611
     612        @Override
     613        public Bounds getLatLonBoundsBox() {
     614            return new Bounds(bounds);
     615        }
     616
     617        @Override
     618        public ProjectionBounds getProjectionBounds() {
     619            return new ProjectionBounds(getProjection().latlon2eastNorth(bounds.getMin()),
     620                    getProjection().latlon2eastNorth(bounds.getMax()));
     621        }
     622    }
    521623}
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    index 13e0392..a82dd1e 100644
    a b package org.openstreetmap.josm.gui.layer;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.Color;
    76import java.awt.Component;
    8 import java.awt.Dimension;
    9 import java.awt.Font;
    10 import java.awt.Graphics;
    117import java.awt.Graphics2D;
    12 import java.awt.GridBagLayout;
    138import java.awt.Image;
    14 import java.awt.Point;
    15 import java.awt.Toolkit;
    169import java.awt.event.ActionEvent;
    17 import java.awt.event.MouseAdapter;
    18 import java.awt.event.MouseEvent;
    19 import java.awt.geom.Point2D;
    20 import java.awt.geom.Rectangle2D;
    21 import java.awt.image.BufferedImage;
    2210import java.awt.image.ImageObserver;
    2311import java.io.File;
    24 import java.io.IOException;
    2512import java.net.MalformedURLException;
    2613import java.net.URL;
    27 import java.text.SimpleDateFormat;
    2814import java.util.ArrayList;
    2915import java.util.Arrays;
    30 import java.util.Collections;
    31 import java.util.Comparator;
    32 import java.util.Date;
    33 import java.util.LinkedList;
     16import java.util.HashMap;
    3417import java.util.List;
    3518import java.util.Map;
    36 import java.util.Map.Entry;
    37 import java.util.Objects;
    3819import java.util.Set;
    3920import java.util.concurrent.ConcurrentSkipListSet;
    4021import java.util.concurrent.atomic.AtomicInteger;
    41 import java.util.function.Consumer;
    42 import java.util.function.Function;
    43 import java.util.stream.Collectors;
    44 import java.util.stream.IntStream;
    45 import java.util.stream.Stream;
    4622
    4723import javax.swing.AbstractAction;
    4824import javax.swing.Action;
    49 import javax.swing.BorderFactory;
    5025import javax.swing.JCheckBoxMenuItem;
    5126import javax.swing.JLabel;
    5227import javax.swing.JMenuItem;
    53 import javax.swing.JOptionPane;
    5428import javax.swing.JPanel;
    5529import javax.swing.JPopupMenu;
    56 import javax.swing.JSeparator;
    57 import javax.swing.JTextField;
    5830import javax.swing.Timer;
    5931
    60 import org.openstreetmap.gui.jmapviewer.AttributionSupport;
    61 import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    6232import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    6333import org.openstreetmap.gui.jmapviewer.Tile;
    6434import org.openstreetmap.gui.jmapviewer.TileXY;
    65 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
    66 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    6735import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
    68 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
    6936import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    7037import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    7138import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    import org.openstreetmap.josm.actions.ImageryAdjustAction;  
    7542import org.openstreetmap.josm.actions.RenameLayerAction;
    7643import org.openstreetmap.josm.actions.SaveActionBase;
    7744import org.openstreetmap.josm.data.Bounds;
    78 import org.openstreetmap.josm.data.ProjectionBounds;
    7945import org.openstreetmap.josm.data.coor.EastNorth;
    8046import org.openstreetmap.josm.data.coor.LatLon;
    8147import org.openstreetmap.josm.data.imagery.ImageryInfo;
    import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;  
    8349import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
    8450import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    8551import org.openstreetmap.josm.data.preferences.IntegerProperty;
    86 import org.openstreetmap.josm.gui.ExtendedDialog;
    8752import org.openstreetmap.josm.gui.MapFrame;
    8853import org.openstreetmap.josm.gui.MapView;
    89 import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
    9054import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
    91 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
    9255import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    9356import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    9457import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
    95 import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;
    9658import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
    9759import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
    9860import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
     61import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter;
    9962import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    10063import org.openstreetmap.josm.gui.util.GuiHelper;
    10164import org.openstreetmap.josm.io.WMSLayerImporter;
    10265import org.openstreetmap.josm.tools.GBC;
    103 import org.openstreetmap.josm.tools.MemoryManager;
    104 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
    105 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
    10666
    10767/**
    10868 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
    import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;  
    11575 * @since 3715
    11676 * @since 8526 (copied from TMSLayer)
    11777 */
    118 public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer
    119 implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener {
     78public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer implements
     79        ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener {
    12080    private static final String PREFERENCE_PREFIX = "imagery.generic";
     81
    12182    /**
    12283     * Registers all setting properties
    12384     */
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    12990    public static final int MAX_ZOOM = 30;
    13091    /** minium zoom level supported */
    13192    public static final int MIN_ZOOM = 2;
    132     private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
    13393
    13494    /** minimum zoom level to show to user */
    13595    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
    13696    /** maximum zoom level to show to user */
    137     public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
     97    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl",
     98            20);
    13899
    139100    //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
    140     /**
    141      * Zoomlevel at which tiles is currently downloaded.
    142      * Initial zoom lvl is set to bestZoom
    143      */
    144     public int currentZoomLevel;
    145 
    146     private final AttributionSupport attribution = new AttributionSupport();
    147     private final TileHolder clickedTileHolder = new TileHolder();
    148101
    149102    /**
    150103     * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    152105     */
    153106    public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0);
    154107
    155     /*
    156      *  use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image)
    157      *  and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible
    158      *  in MapView (for example - when limiting min zoom in imagery)
    159      *
    160      *  Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached
    161      */
    162     protected TileCache tileCache; // initialized together with tileSource
    163     protected T tileSource;
    164     protected TileLoader tileLoader;
    165 
    166108    /**
    167109     * A timer that is used to delay invalidation events if required.
    168110     */
    169111    private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate());
    170112
    171     private final MouseAdapter adapter = new MouseAdapter() {
    172         @Override
    173         public void mouseClicked(MouseEvent e) {
    174             if (!isVisible()) return;
    175             if (e.getButton() == MouseEvent.BUTTON3) {
    176                 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
    177                 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY());
    178             } else if (e.getButton() == MouseEvent.BUTTON1) {
    179                 attribution.handleAttribution(e.getPoint(), true);
    180             }
    181         }
    182     };
    183 
    184113    private final TileSourceDisplaySettings displaySettings = createDisplaySettings();
    185114
    186115    private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
    187     // prepared to be moved to the painter
    188     private TileCoordinateConverter coordinateConverter;
     116
     117    private HashMap<MapView, TileSourcePainter<T>> painters = new HashMap<>();
    189118
    190119    /**
    191120     * Creates Tile Source based Imagery Layer based on Imagery Info
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    222151        invalidate();
    223152    }
    224153
    225     protected abstract TileLoaderFactory getTileLoaderFactory();
    226 
    227154    /**
    228      *
    229      * @param info imagery info
    230      * @return TileSource for specified ImageryInfo
    231      * @throws IllegalArgumentException when Imagery is not supported by layer
     155     * Generate the tile loader
     156     * @param tileSource A tile source that is already generated.
     157     * @return The tile loader.
    232158     */
    233     protected abstract T getTileSource(ImageryInfo info);
    234 
    235     protected Map<String, String> getHeaders(T tileSource) {
    236         if (tileSource instanceof TemplatedTileSource) {
    237             return ((TemplatedTileSource) tileSource).getHeaders();
    238         }
    239         return null;
    240     }
    241 
    242     protected void initTileSource(T tileSource) {
    243         coordinateConverter = new TileCoordinateConverter(Main.map.mapView, tileSource, getDisplaySettings());
    244         attribution.initialize(tileSource);
    245 
    246         currentZoomLevel = getBestZoom();
    247 
     159    public TileLoader generateTileLoader(T tileSource) {
    248160        Map<String, String> headers = getHeaders(tileSource);
    249161
    250         tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
     162        TileLoader loader = getTileLoaderFactory().makeTileLoader(this, headers);
     163        if (loader != null) {
     164            return loader;
     165        }
    251166
    252167        try {
    253168            if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) {
    254                 tileLoader = new OsmTileLoader(this);
     169                return new OsmTileLoader(this);
    255170            }
    256171        } catch (MalformedURLException e) {
    257172            // ignore, assume that this is not a file
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    260175            }
    261176        }
    262177
    263         if (tileLoader == null)
    264             tileLoader = new OsmTileLoader(this, headers);
     178        return new OsmTileLoader(this, headers);
     179    }
     180
     181    /**
     182     * Generates the tile source for this layer.
     183     * @return The tile source
     184     */
     185    public T getTileSource() {
     186        return getTileSource(getInfo());
     187    }
     188
     189    protected abstract TileLoaderFactory getTileLoaderFactory();
     190
     191    /**
     192     * Used by the default {@link TileSourcePainter} to create the tile source.
     193     * @param info imagery info
     194     * @return TileSource for specified ImageryInfo
     195     * @throws IllegalArgumentException when Imagery is not supported by layer
     196     */
     197    protected T getTileSource(ImageryInfo info) {
     198        throw new UnsupportedOperationException();
     199    }
    265200
    266         tileCache = new MemoryTileCache(estimateTileCacheSize());
     201    protected Map<String, String> getHeaders(T tileSource) {
     202        if (tileSource instanceof TemplatedTileSource) {
     203            return ((TemplatedTileSource) tileSource).getHeaders();
     204        }
     205        return null;
    267206    }
    268207
    269208    @Override
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    280219    }
    281220
    282221    /**
    283      * Clears the tile cache.
    284      *
    285      * If the current tileLoader is an instance of OsmTileLoader, a new
    286      * TmsTileClearController is created and passed to the according clearCache
    287      * method.
    288      *
    289      * @param monitor not used in this implementation - as cache clear is instaneus
    290      */
    291     public void clearTileCache(ProgressMonitor monitor) {
    292         if (tileLoader instanceof CachedTileLoader) {
    293             ((CachedTileLoader) tileLoader).clearCache(tileSource);
    294         }
    295         tileCache.clear();
    296     }
    297 
    298     /**
    299222     * Initiates a repaint of Main.map
    300223     *
    301224     * @see Main#map
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    361284        return adjustAction;
    362285    }
    363286
    364     /**
    365      * Returns average number of screen pixels per tile pixel for current mapview
    366      * @param zoom zoom level
    367      * @return average number of screen pixels per tile pixel
    368      */
    369     private double getScaleFactor(int zoom) {
    370         if (coordinateConverter != null) {
    371             return coordinateConverter.getScaleFactor(zoom);
    372         } else {
    373             return 1;
    374         }
    375     }
    376 
    377     protected int getBestZoom() {
    378         double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
    379         double result = Math.log(factor)/Math.log(2)/2;
    380         /*
    381          * Math.log(factor)/Math.log(2) - gives log base 2 of factor
    382          * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
    383          *
    384          * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET
    385          * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET
    386          * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or
    387          * maps as a imagery layer
    388          */
    389 
    390         int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
    391 
    392         intResult = Math.min(intResult, getMaxZoomLvl());
    393         intResult = Math.max(intResult, getMinZoomLvl());
    394         return intResult;
    395     }
    396 
    397287    private static boolean actionSupportLayers(List<Layer> layers) {
    398288        return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
    399289    }
    400290
    401     private final class ShowTileInfoAction extends AbstractAction {
    402 
    403         private ShowTileInfoAction() {
    404             super(tr("Show tile info"));
    405         }
    406 
    407         private String getSizeString(int size) {
    408             StringBuilder ret = new StringBuilder();
    409             return ret.append(size).append('x').append(size).toString();
    410         }
    411 
    412         private JTextField createTextField(String text) {
    413             JTextField ret = new JTextField(text);
    414             ret.setEditable(false);
    415             ret.setBorder(BorderFactory.createEmptyBorder());
    416             return ret;
    417         }
    418 
    419         @Override
    420         public void actionPerformed(ActionEvent ae) {
    421             Tile clickedTile = clickedTileHolder.getTile();
    422             if (clickedTile != null) {
    423                 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
    424                 JPanel panel = new JPanel(new GridBagLayout());
    425                 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile);
    426                 String url = "";
    427                 try {
    428                     url = clickedTile.getUrl();
    429                 } catch (IOException e) {
    430                     // silence exceptions
    431                     Main.trace(e);
    432                 }
    433 
    434                 String[][] content = {
    435                         {"Tile name", clickedTile.getKey()},
    436                         {"Tile url", url},
    437                         {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
    438                         {"Tile display size", new StringBuilder().append(displaySize.getWidth())
    439                                                                  .append('x')
    440                                                                  .append(displaySize.getHeight()).toString()},
    441                 };
    442 
    443                 for (String[] entry: content) {
    444                     panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
    445                     panel.add(GBC.glue(5, 0), GBC.std());
    446                     panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
    447                 }
    448 
    449                 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
    450                     panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());
    451                     panel.add(GBC.glue(5, 0), GBC.std());
    452                     String value = e.getValue();
    453                     if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
    454                         value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
    455                     }
    456                     panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
    457 
    458                 }
    459                 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
    460                 ed.setContent(panel);
    461                 ed.showDialog();
    462             }
    463         }
    464     }
    465 
    466     private final class LoadTileAction extends AbstractAction {
    467 
    468         private LoadTileAction() {
    469             super(tr("Load tile"));
    470         }
    471 
    472         @Override
    473         public void actionPerformed(ActionEvent ae) {
    474             Tile clickedTile = clickedTileHolder.getTile();
    475             if (clickedTile != null) {
    476                 loadTile(clickedTile, true);
    477                 invalidate();
    478             }
    479         }
    480     }
    481 
    482291    private class AutoZoomAction extends AbstractAction implements LayerAction {
    483292        AutoZoomAction() {
    484293            super(tr("Auto zoom"));
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    548357        }
    549358    }
    550359
    551     private class LoadAllTilesAction extends AbstractAction {
    552         LoadAllTilesAction() {
    553             super(tr("Load all tiles"));
    554         }
    555 
    556         @Override
    557         public void actionPerformed(ActionEvent ae) {
    558             loadAllTiles(true);
    559         }
    560     }
    561 
    562     private class LoadErroneusTilesAction extends AbstractAction {
    563         LoadErroneusTilesAction() {
    564             super(tr("Load all error tiles"));
    565         }
    566 
    567         @Override
    568         public void actionPerformed(ActionEvent ae) {
    569             loadAllErrorTiles(true);
    570         }
    571     }
    572 
    573     private class ZoomToNativeLevelAction extends AbstractAction {
    574         ZoomToNativeLevelAction() {
    575             super(tr("Zoom to native resolution"));
    576         }
    577 
    578         @Override
    579         public void actionPerformed(ActionEvent ae) {
    580             double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
    581             Main.map.mapView.zoomToFactor(newFactor);
    582             redraw();
    583         }
    584     }
    585 
    586     private class ZoomToBestAction extends AbstractAction {
    587         ZoomToBestAction() {
    588             super(tr("Change resolution"));
    589             setEnabled(!getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel);
    590         }
    591 
    592         @Override
    593         public void actionPerformed(ActionEvent ae) {
    594             setZoomLevel(getBestZoom());
    595         }
    596     }
    597 
    598     private class IncreaseZoomAction extends AbstractAction {
    599         IncreaseZoomAction() {
    600             super(tr("Increase zoom"));
    601             setEnabled(!getDisplaySettings().isAutoZoom() && zoomIncreaseAllowed());
    602         }
    603 
    604         @Override
    605         public void actionPerformed(ActionEvent ae) {
    606             increaseZoomLevel();
    607         }
    608     }
    609 
    610     private class DecreaseZoomAction extends AbstractAction {
    611         DecreaseZoomAction() {
    612             super(tr("Decrease zoom"));
    613             setEnabled(!getDisplaySettings().isAutoZoom() && zoomDecreaseAllowed());
    614         }
    615 
    616         @Override
    617         public void actionPerformed(ActionEvent ae) {
    618             decreaseZoomLevel();
    619         }
    620     }
    621 
    622     private class FlushTileCacheAction extends AbstractAction {
    623         FlushTileCacheAction() {
    624             super(tr("Flush tile cache"));
    625             setEnabled(tileLoader instanceof CachedTileLoader);
    626         }
    627 
    628         @Override
    629         public void actionPerformed(ActionEvent ae) {
    630             new PleaseWaitRunnable(tr("Flush tile cache")) {
    631                 @Override
    632                 protected void realRun() {
    633                     clearTileCache(getProgressMonitor());
    634                 }
    635 
    636                 @Override
    637                 protected void finish() {
    638                     // empty - flush is instaneus
    639                 }
    640 
    641                 @Override
    642                 protected void cancel() {
    643                     // empty - flush is instaneus
    644                 }
    645             }.run();
    646         }
    647     }
    648 
    649     /**
    650      * Simple class to keep clickedTile within hookUpMapView
    651      */
    652     private static final class TileHolder {
    653         private Tile t;
    654 
    655         public Tile getTile() {
    656             return t;
    657         }
    658 
    659         public void setTile(Tile t) {
    660             this.t = t;
    661         }
    662     }
    663 
    664     /**
    665      * Creates popup menu items and binds to mouse actions
    666      */
    667360    @Override
    668361    public void hookUpMapView() {
    669         // this needs to be here and not in constructor to allow empty TileSource class construction
    670         // using SessionWriter
    671         initializeIfRequired();
    672 
    673         super.hookUpMapView();
    674362    }
    675363
    676364    @Override
    677365    public LayerPainter attachToMapView(MapViewEvent event) {
    678         initializeIfRequired();
    679 
    680         event.getMapView().addMouseListener(adapter);
     366        GuiHelper.assertCallFromEdt();
    681367        MapView.addZoomChangeListener(this);
    682368
    683369        if (this instanceof NativeScaleLayer) {
    684370            event.getMapView().setNativeScaleLayer((NativeScaleLayer) this);
    685371        }
    686372
    687         // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not
    688         // start loading.
    689         // FIXME: Check if this is still required.
    690         event.getMapView().repaint(500);
    691 
    692         return super.attachToMapView(event);
    693     }
    694 
    695     private void initializeIfRequired() {
    696         if (tileSource == null) {
    697             tileSource = getTileSource(info);
    698             if (tileSource == null) {
    699                 throw new IllegalArgumentException(tr("Failed to create tile source"));
    700             }
    701             // check if projection is supported
    702             projectionChanged(null, Main.getProjection());
    703             initTileSource(this.tileSource);
    704         }
     373        TileSourcePainter<T> painter = createMapViewPainter(event);
     374        painters.put(event.getMapView(), painter);
     375        return painter;
    705376    }
    706377
    707378    @Override
    708     protected LayerPainter createMapViewPainter(MapViewEvent event) {
    709         return new TileSourcePainter();
     379    protected TileSourcePainter<T> createMapViewPainter(MapViewEvent event) {
     380        return new TileSourcePainter<>(this, event.getMapView());
    710381    }
    711382
    712383    /**
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    715386    public class TileSourceLayerPopup extends JPopupMenu {
    716387        /**
    717388         * Constructs a new {@code TileSourceLayerPopup}.
     389         * @param mv
    718390         */
    719         public TileSourceLayerPopup() {
     391        public TileSourceLayerPopup(MapView mv) {
    720392            for (Action a : getCommonEntries()) {
    721                 if (a instanceof LayerAction) {
    722                     add(((LayerAction) a).createMenuComponent());
    723                 } else {
    724                     add(new JMenuItem(a));
    725                 }
     393                addAction(a);
     394            }
     395            for (Action a : getMapViewEntries(mv)) {
     396                addAction(a);
    726397            }
    727             add(new JSeparator());
    728             add(new JMenuItem(new LoadTileAction()));
    729             add(new JMenuItem(new ShowTileInfoAction()));
    730398        }
    731     }
    732399
    733     protected int estimateTileCacheSize() {
    734         Dimension screenSize = GuiHelper.getMaximumScreenSize();
    735         int height = screenSize.height;
    736         int width = screenSize.width;
    737         int tileSize = 256; // default tile size
    738         if (tileSource != null) {
    739             tileSize = tileSource.getTileSize();
     400        private void addAction(Action a) {
     401            if (a instanceof LayerAction) {
     402                add(((LayerAction) a).createMenuComponent());
     403            } else {
     404                add(new JMenuItem(a));
     405            }
    740406        }
    741         // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
    742         int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));
    743         // add 10% for tiles from different zoom levels
    744         int ret = (int) Math.ceil(
    745                 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
    746                 * 4);
    747         Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);
    748         return ret;
    749407    }
    750408
    751409    @Override
    752410    public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
    753         if (tileSource == null) {
    754             return;
    755         }
    756411        switch (e.getChangedSetting()) {
    757         case TileSourceDisplaySettings.AUTO_ZOOM:
    758             if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {
    759                 setZoomLevel(getBestZoom());
    760                 invalidate();
    761             }
    762             break;
    763412        case TileSourceDisplaySettings.AUTO_LOAD:
    764413            if (getDisplaySettings().isAutoLoad()) {
    765414                invalidate();
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    847496     */
    848497    @Override
    849498    public void zoomChanged() {
    850         if (Main.isDebugEnabled()) {
    851             Main.debug("zoomChanged(): " + currentZoomLevel);
    852         }
    853         if (tileLoader instanceof TMSCachedTileLoader) {
    854             ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
    855         }
    856         invalidate();
    857     }
    858 
    859     protected int getMaxZoomLvl() {
    860         if (info.getMaxZoom() != 0)
    861             return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
    862         else
    863             return getMaxZoomLvl(tileSource);
    864     }
    865 
    866     protected int getMinZoomLvl() {
    867         if (info.getMinZoom() != 0)
    868             return checkMinZoomLvl(info.getMinZoom(), tileSource);
    869         else
    870             return getMinZoomLvl(tileSource);
    871     }
    872 
    873     /**
    874      *
    875      * @return if its allowed to zoom in
    876      */
    877     public boolean zoomIncreaseAllowed() {
    878         boolean zia = currentZoomLevel < this.getMaxZoomLvl();
    879         if (Main.isDebugEnabled()) {
    880             Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
    881         }
    882         return zia;
    883     }
    884 
    885     /**
    886      * Zoom in, go closer to map.
    887      *
    888      * @return    true, if zoom increasing was successful, false otherwise
    889      */
    890     public boolean increaseZoomLevel() {
    891         if (zoomIncreaseAllowed()) {
    892             currentZoomLevel++;
    893             if (Main.isDebugEnabled()) {
    894                 Main.debug("increasing zoom level to: " + currentZoomLevel);
    895             }
    896             zoomChanged();
    897         } else {
    898             Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
    899                     "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
    900             return false;
    901         }
    902         return true;
    903     }
    904 
    905     /**
    906      * Sets the zoom level of the layer
    907      * @param zoom zoom level
    908      * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
    909      */
    910     public boolean setZoomLevel(int zoom) {
    911         if (zoom == currentZoomLevel) return true;
    912         if (zoom > this.getMaxZoomLvl()) return false;
    913         if (zoom < this.getMinZoomLvl()) return false;
    914         currentZoomLevel = zoom;
    915         zoomChanged();
    916         return true;
    917     }
    918 
    919     /**
    920      * Check if zooming out is allowed
    921      *
    922      * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
    923      */
    924     public boolean zoomDecreaseAllowed() {
    925         boolean zda = currentZoomLevel > this.getMinZoomLvl();
    926         if (Main.isDebugEnabled()) {
    927             Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoomLvl());
    928         }
    929         return zda;
    930     }
    931 
    932     /**
    933      * Zoom out from map.
    934      *
    935      * @return    true, if zoom increasing was successfull, false othervise
    936      */
    937     public boolean decreaseZoomLevel() {
    938         if (zoomDecreaseAllowed()) {
    939             if (Main.isDebugEnabled()) {
    940                 Main.debug("decreasing zoom level to: " + currentZoomLevel);
    941             }
    942             currentZoomLevel--;
    943             zoomChanged();
    944         } else {
    945             return false;
    946         }
    947         return true;
    948     }
    949 
    950     /*
    951      * We use these for quick, hackish calculations.  They
    952      * are temporary only and intentionally not inserted
    953      * into the tileCache.
    954      */
    955     private Tile tempCornerTile(Tile t) {
    956         int x = t.getXtile() + 1;
    957         int y = t.getYtile() + 1;
    958         int zoom = t.getZoom();
    959         Tile tile = getTile(x, y, zoom);
    960         if (tile != null)
    961             return tile;
    962         return new Tile(tileSource, x, y, zoom);
    963     }
    964 
    965     private Tile getOrCreateTile(TilePosition tilePosition) {
    966         return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
    967     }
    968 
    969     private Tile getOrCreateTile(int x, int y, int zoom) {
    970         Tile tile = getTile(x, y, zoom);
    971         if (tile == null) {
    972             tile = new Tile(tileSource, x, y, zoom);
    973             tileCache.addTile(tile);
    974         }
    975 
    976         if (!tile.isLoaded()) {
    977             tile.loadPlaceholderFromCache(tileCache);
    978         }
    979         return tile;
    980     }
    981 
    982     private Tile getTile(TilePosition tilePosition) {
    983         return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
    984     }
    985 
    986     /**
    987      * Returns tile at given position.
    988      * This can and will return null for tiles that are not already in the cache.
    989      * @param x tile number on the x axis of the tile to be retrieved
    990      * @param y tile number on the y axis of the tile to be retrieved
    991      * @param zoom zoom level of the tile to be retrieved
    992      * @return tile at given position
    993      */
    994     private Tile getTile(int x, int y, int zoom) {
    995         if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)
    996          || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))
    997             return null;
    998         return tileCache.getTile(tileSource, x, y, zoom);
    999     }
    1000 
    1001     private boolean loadTile(Tile tile, boolean force) {
    1002         if (tile == null)
    1003             return false;
    1004         if (!force && (tile.isLoaded() || tile.hasError()))
    1005             return false;
    1006         if (tile.isLoading())
    1007             return false;
    1008         tileLoader.createTileLoaderJob(tile).submit(force);
    1009         return true;
    1010     }
    1011 
    1012     private TileSet getVisibleTileSet() {
    1013         MapView mv = Main.map.mapView;
    1014         MapViewRectangle area = mv.getState().getViewArea();
    1015         ProjectionBounds bounds = area.getProjectionBounds();
    1016         return getTileSet(bounds.getMin(), bounds.getMax(), currentZoomLevel);
    1017     }
    1018 
    1019     protected void loadAllTiles(boolean force) {
    1020         TileSet ts = getVisibleTileSet();
    1021 
    1022         // if there is more than 18 tiles on screen in any direction, do not load all tiles!
    1023         if (ts.tooLarge()) {
    1024             Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
    1025             return;
    1026         }
    1027         ts.loadAllTiles(force);
    1028         invalidate();
    1029     }
    1030 
    1031     protected void loadAllErrorTiles(boolean force) {
    1032         TileSet ts = getVisibleTileSet();
    1033         ts.loadAllErrorTiles(force);
    1034499        invalidate();
    1035500    }
    1036501
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1061526        });
    1062527    }
    1063528
    1064     private boolean imageLoaded(Image i) {
    1065         if (i == null)
    1066             return false;
    1067         int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
    1068         if ((status & ALLBITS) != 0)
    1069             return true;
    1070         return false;
    1071     }
    1072 
    1073     /**
    1074      * Returns the image for the given tile image is loaded.
    1075      * Otherwise returns  null.
    1076      *
    1077      * @param tile the Tile for which the image should be returned
    1078      * @return  the image of the tile or null.
    1079      */
    1080     private Image getLoadedTileImage(Tile tile) {
    1081         Image img = tile.getImage();
    1082         if (!imageLoaded(img))
    1083             return null;
    1084         return img;
    1085     }
    1086 
    1087     // 'source' is the pixel coordinates for the area that
    1088     // the img is capable of filling in.  However, we probably
    1089     // only want a portion of it.
    1090     //
    1091     // 'border' is the screen cordinates that need to be drawn.
    1092     //  We must not draw outside of it.
    1093     private void drawImageInside(Graphics g, Image sourceImg, Rectangle2D source, Rectangle2D border) {
    1094         Rectangle2D target = source;
    1095 
    1096         // If a border is specified, only draw the intersection
    1097         // if what we have combined with what we are supposed to draw.
    1098         if (border != null) {
    1099             target = source.createIntersection(border);
    1100             if (Main.isDebugEnabled()) {
    1101                 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
    1102             }
    1103         }
    1104 
    1105         // All of the rectangles are in screen coordinates.  We need
    1106         // to how these correlate to the sourceImg pixels.  We could
    1107         // avoid doing this by scaling the image up to the 'source' size,
    1108         // but this should be cheaper.
    1109         //
    1110         // In some projections, x any y are scaled differently enough to
    1111         // cause a pixel or two of fudge.  Calculate them separately.
    1112         double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
    1113         double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
    1114 
    1115         // How many pixels into the 'source' rectangle are we drawing?
    1116         double screenXoffset = target.getX() - source.getX();
    1117         double screenYoffset = target.getY() - source.getY();
    1118         // And how many pixels into the image itself does that correlate to?
    1119         int imgXoffset = (int) (screenXoffset * imageXScaling + 0.5);
    1120         int imgYoffset = (int) (screenYoffset * imageYScaling + 0.5);
    1121         // Now calculate the other corner of the image that we need
    1122         // by scaling the 'target' rectangle's dimensions.
    1123         int imgXend = imgXoffset + (int) (target.getWidth() * imageXScaling + 0.5);
    1124         int imgYend = imgYoffset + (int) (target.getHeight() * imageYScaling + 0.5);
    1125 
    1126         if (Main.isDebugEnabled()) {
    1127             Main.debug("drawing image into target rect: " + target);
    1128         }
    1129         g.drawImage(sourceImg,
    1130                 (int) target.getX(), (int) target.getY(),
    1131                 (int) target.getMaxX(), (int) target.getMaxY(),
    1132                 imgXoffset, imgYoffset,
    1133                 imgXend, imgYend,
    1134                 this);
    1135         if (PROP_FADE_AMOUNT.get() != 0) {
    1136             // dimm by painting opaque rect...
    1137             g.setColor(getFadeColorWithAlpha());
    1138             ((Graphics2D) g).fill(target);
    1139         }
    1140     }
    1141 
    1142     private List<Tile> paintTileImages(Graphics g, TileSet ts) {
    1143         Object paintMutex = new Object();
    1144         List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
    1145         ts.visitTiles(tile -> {
    1146             Image img = getLoadedTileImage(tile);
    1147             if (img == null) {
    1148                 missed.add(new TilePosition(tile));
    1149             }
    1150             img = applyImageProcessors((BufferedImage) img);
    1151             Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
    1152             synchronized (paintMutex) {
    1153                 //cannot paint in parallel
    1154                 drawImageInside(g, img, sourceRect, null);
    1155             }
    1156         }, missed::add);
    1157 
    1158         return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
    1159     }
    1160 
    1161     // This function is called for several zoom levels, not just
    1162     // the current one.  It should not trigger any tiles to be
    1163     // downloaded.  It should also avoid polluting the tile cache
    1164     // with any tiles since these tiles are not mandatory.
    1165     //
    1166     // The "border" tile tells us the boundaries of where we may
    1167     // draw.  It will not be from the zoom level that is being
    1168     // drawn currently.  If drawing the displayZoomLevel,
    1169     // border is null and we draw the entire tile set.
    1170     private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
    1171         if (zoom <= 0) return Collections.emptyList();
    1172         Rectangle2D borderRect = coordinateConverter.getRectangleForTile(border);
    1173         List<Tile> missedTiles = new LinkedList<>();
    1174         // The callers of this code *require* that we return any tiles
    1175         // that we do not draw in missedTiles.  ts.allExistingTiles() by
    1176         // default will only return already-existing tiles.  However, we
    1177         // need to return *all* tiles to the callers, so force creation here.
    1178         for (Tile tile : ts.allTilesCreate()) {
    1179             Image img = getLoadedTileImage(tile);
    1180             if (img == null || tile.hasError()) {
    1181                 if (Main.isDebugEnabled()) {
    1182                     Main.debug("missed tile: " + tile);
    1183                 }
    1184                 missedTiles.add(tile);
    1185                 continue;
    1186             }
    1187 
    1188             // applying all filters to this layer
    1189             img = applyImageProcessors((BufferedImage) img);
    1190 
    1191             Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
    1192             if (borderRect != null && !sourceRect.intersects(borderRect)) {
    1193                 continue;
    1194             }
    1195             drawImageInside(g, img, sourceRect, borderRect);
    1196         }
    1197         return missedTiles;
    1198     }
    1199 
    1200     private void myDrawString(Graphics g, String text, int x, int y) {
    1201         Color oldColor = g.getColor();
    1202         String textToDraw = text;
    1203         if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
    1204             // text longer than tile size, split it
    1205             StringBuilder line = new StringBuilder();
    1206             StringBuilder ret = new StringBuilder();
    1207             for (String s: text.split(" ")) {
    1208                 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
    1209                     ret.append(line).append('\n');
    1210                     line.setLength(0);
    1211                 }
    1212                 line.append(s).append(' ');
    1213             }
    1214             ret.append(line);
    1215             textToDraw = ret.toString();
    1216         }
    1217         int offset = 0;
    1218         for (String s: textToDraw.split("\n")) {
    1219             g.setColor(Color.black);
    1220             g.drawString(s, x + 1, y + offset + 1);
    1221             g.setColor(oldColor);
    1222             g.drawString(s, x, y + offset);
    1223             offset += g.getFontMetrics().getHeight() + 3;
    1224         }
    1225     }
    1226 
    1227     private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
    1228         if (tile == null) {
    1229             return;
    1230         }
    1231         Point2D p = coordinateConverter.getPixelForTile(t);
    1232         int fontHeight = g.getFontMetrics().getHeight();
    1233         int x = (int) p.getX();
    1234         int y = (int) p.getY();
    1235         int texty = y + 2 + fontHeight;
    1236 
    1237         /*if (PROP_DRAW_DEBUG.get()) {
    1238             myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
    1239             texty += 1 + fontHeight;
    1240             if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
    1241                 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
    1242                 texty += 1 + fontHeight;
    1243             }
    1244         }*/
    1245 
    1246         /*String tileStatus = tile.getStatus();
    1247         if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
    1248             myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
    1249             texty += 1 + fontHeight;
    1250         }*/
    1251 
    1252         if (tile.hasError() && getDisplaySettings().isShowErrors()) {
    1253             myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
    1254             //texty += 1 + fontHeight;
    1255         }
    1256 
    1257         int xCursor = -1;
    1258         int yCursor = -1;
    1259         if (Main.isDebugEnabled()) {
    1260             if (yCursor < t.getYtile()) {
    1261                 if (t.getYtile() % 32 == 31) {
    1262                     g.fillRect(0, y - 1, mv.getWidth(), 3);
    1263                 } else {
    1264                     g.drawLine(0, y, mv.getWidth(), y);
    1265                 }
    1266                 //yCursor = t.getYtile();
    1267             }
    1268             // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
    1269             if (xCursor < t.getXtile()) {
    1270                 if (t.getXtile() % 32 == 0) {
    1271                     // level 7 tile boundary
    1272                     g.fillRect(x - 1, 0, 3, mv.getHeight());
    1273                 } else {
    1274                     g.drawLine(x, 0, x, mv.getHeight());
    1275                 }
    1276                 //xCursor = t.getXtile();
    1277             }
    1278         }
    1279     }
    1280 
    1281     private LatLon getShiftedLatLon(EastNorth en) {
    1282         return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
    1283     }
    1284 
    1285     private ICoordinate getShiftedCoord(EastNorth en) {
    1286         return getShiftedLatLon(en).toCoordinate();
    1287     }
    1288 
    1289     private LatLon getShiftedLatLon(ICoordinate latLon) {
    1290         return getShiftedLatLon(Main.getProjection().latlon2eastNorth(new LatLon(latLon)));
    1291     }
    1292 
    1293 
    1294     private final TileSet nullTileSet = new TileSet();
    1295 
    1296     /**
    1297      * This is a rectangular range of tiles.
    1298      */
    1299     private static class TileRange {
    1300         int minX;
    1301         int maxX;
    1302         int minY;
    1303         int maxY;
    1304         int zoom;
    1305 
    1306         private TileRange() {
    1307         }
    1308 
    1309         protected TileRange(TileXY t1, TileXY t2, int zoom) {
    1310             minX = (int) Math.floor(Math.min(t1.getX(), t2.getX()));
    1311             minY = (int) Math.floor(Math.min(t1.getY(), t2.getY()));
    1312             maxX = (int) Math.ceil(Math.max(t1.getX(), t2.getX()));
    1313             maxY = (int) Math.ceil(Math.max(t1.getY(), t2.getY()));
    1314             this.zoom = zoom;
    1315         }
    1316 
    1317         protected double tilesSpanned() {
    1318             return Math.sqrt(1.0 * this.size());
    1319         }
    1320 
    1321         protected int size() {
    1322             int xSpan = maxX - minX + 1;
    1323             int ySpan = maxY - minY + 1;
    1324             return xSpan * ySpan;
    1325         }
    1326 
    1327         /**
    1328          * Gets a stream of all tile positions in this set
    1329          * @return A stream of all positions
    1330          */
    1331         public Stream<TilePosition> tilePositions() {
    1332             if (zoom == 0) {
    1333                 return Stream.empty();
    1334             } else {
    1335                 return IntStream.rangeClosed(minX, maxX).mapToObj(
    1336                         x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
    1337                         ).flatMap(Function.identity());
    1338             }
    1339         }
    1340     }
    1341 
    1342     /**
    1343      * The position of a single tile.
    1344      * @author Michael Zangl
    1345      * @since xxx
    1346      */
    1347     private static class TilePosition {
    1348         private final int x;
    1349         private final int y;
    1350         private final int zoom;
    1351         TilePosition(int x, int y, int zoom) {
    1352             super();
    1353             this.x = x;
    1354             this.y = y;
    1355             this.zoom = zoom;
    1356         }
    1357 
    1358         TilePosition(Tile tile) {
    1359             this(tile.getXtile(), tile.getYtile(), tile.getZoom());
    1360         }
    1361 
    1362         /**
    1363          * @return the x position
    1364          */
    1365         public int getX() {
    1366             return x;
    1367         }
    1368 
    1369         /**
    1370          * @return the y position
    1371          */
    1372         public int getY() {
    1373             return y;
    1374         }
    1375 
    1376         /**
    1377          * @return the zoom
    1378          */
    1379         public int getZoom() {
    1380             return zoom;
    1381         }
    1382 
    1383         @Override
    1384         public String toString() {
    1385             return "TilePosition [x=" + x + ", y=" + y + ", zoom=" + zoom + "]";
    1386         }
    1387     }
    1388 
    1389     private class TileSet extends TileRange {
    1390 
    1391         protected TileSet(TileXY t1, TileXY t2, int zoom) {
    1392             super(t1, t2, zoom);
    1393             sanitize();
    1394         }
    1395 
    1396         /**
    1397          * null tile set
    1398          */
    1399         private TileSet() {
    1400             // default
    1401         }
    1402 
    1403         protected void sanitize() {
    1404             if (minX < tileSource.getTileXMin(zoom)) {
    1405                 minX = tileSource.getTileXMin(zoom);
    1406             }
    1407             if (minY < tileSource.getTileYMin(zoom)) {
    1408                 minY = tileSource.getTileYMin(zoom);
    1409             }
    1410             if (maxX > tileSource.getTileXMax(zoom)) {
    1411                 maxX = tileSource.getTileXMax(zoom);
    1412             }
    1413             if (maxY > tileSource.getTileYMax(zoom)) {
    1414                 maxY = tileSource.getTileYMax(zoom);
    1415             }
    1416         }
    1417 
    1418         private boolean tooSmall() {
    1419             return this.tilesSpanned() < 2.1;
    1420         }
    1421 
    1422         private boolean tooLarge() {
    1423             return insane() || this.tilesSpanned() > 20;
    1424         }
    1425 
    1426         private boolean insane() {
    1427             return tileCache == null || size() > tileCache.getCacheSize();
    1428         }
    1429 
    1430         /**
    1431          * Get all tiles represented by this TileSet that are already in the tileCache.
    1432          */
    1433         private List<Tile> allExistingTiles() {
    1434             return allTiles(p -> getTile(p));
    1435         }
    1436 
    1437         private List<Tile> allTilesCreate() {
    1438             return allTiles(p -> getOrCreateTile(p));
    1439         }
    1440 
    1441         private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
    1442             return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
    1443         }
    1444 
    1445         @Override
    1446         public Stream<TilePosition> tilePositions() {
    1447             if (this.insane()) {
    1448                 // Tileset is either empty or too large
    1449                 return Stream.empty();
    1450             } else {
    1451                 return super.tilePositions();
    1452             }
    1453         }
    1454 
    1455         private List<Tile> allLoadedTiles() {
    1456             return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
    1457         }
    1458 
    1459         /**
    1460          * @return comparator, that sorts the tiles from the center to the edge of the current screen
    1461          */
    1462         private Comparator<Tile> getTileDistanceComparator() {
    1463             final int centerX = (int) Math.ceil((minX + maxX) / 2d);
    1464             final int centerY = (int) Math.ceil((minY + maxY) / 2d);
    1465             return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
    1466         }
    1467 
    1468         private void loadAllTiles(boolean force) {
    1469             if (!getDisplaySettings().isAutoLoad() && !force)
    1470                 return;
    1471             List<Tile> allTiles = allTilesCreate();
    1472             allTiles.sort(getTileDistanceComparator());
    1473             for (Tile t : allTiles) {
    1474                 loadTile(t, force);
    1475             }
    1476         }
    1477 
    1478         private void loadAllErrorTiles(boolean force) {
    1479             if (!getDisplaySettings().isAutoLoad() && !force)
    1480                 return;
    1481             for (Tile t : this.allTilesCreate()) {
    1482                 if (t.hasError()) {
    1483                     tileLoader.createTileLoaderJob(t).submit(force);
    1484                 }
    1485             }
    1486         }
    1487 
    1488         /**
    1489          * Call the given paint method for all tiles in this tile set.
    1490          * <p>
    1491          * Uses a parallel stream.
    1492          * @param visitor A visitor to call for each tile.
    1493          * @param missed a consumer to call for each missed tile.
    1494          */
    1495         public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
    1496             tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
    1497         }
    1498 
    1499         private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
    1500             Tile tile = getTile(tp);
    1501             if (tile == null) {
    1502                 missed.accept(tp);
    1503             } else {
    1504                 visitor.accept(tile);
    1505             }
    1506         }
    1507 
    1508         @Override
    1509         public String toString() {
    1510             return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
    1511         }
    1512     }
    1513 
    1514     /**
    1515      * Create a TileSet by EastNorth bbox taking a layer shift in account
    1516      * @param topLeft top-left lat/lon
    1517      * @param botRight bottom-right lat/lon
    1518      * @param zoom zoom level
    1519      * @return the tile set
    1520      * @since 10651
    1521      */
    1522     protected TileSet getTileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
    1523         return getTileSet(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
    1524     }
    1525 
    1526     /**
    1527      * Create a TileSet by known LatLon bbox without layer shift correction
    1528      * @param topLeft top-left lat/lon
    1529      * @param botRight bottom-right lat/lon
    1530      * @param zoom zoom level
    1531      * @return the tile set
    1532      * @since 10651
    1533      */
    1534     protected TileSet getTileSet(LatLon topLeft, LatLon botRight, int zoom) {
    1535         if (zoom == 0)
    1536             return new TileSet();
    1537 
    1538         TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
    1539         TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
    1540         return new TileSet(t1, t2, zoom);
    1541     }
    1542 
    1543     private static class TileSetInfo {
    1544         public boolean hasVisibleTiles;
    1545         public boolean hasOverzoomedTiles;
    1546         public boolean hasLoadingTiles;
    1547     }
    1548 
    1549     private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
    1550         List<Tile> allTiles = ts.allExistingTiles();
    1551         TileSetInfo result = new TileSetInfo();
    1552         result.hasLoadingTiles = allTiles.size() < ts.size();
    1553         for (Tile t : allTiles) {
    1554             if ("no-tile".equals(t.getValue("tile-info"))) {
    1555                 result.hasOverzoomedTiles = true;
    1556             }
    1557 
    1558             if (t.isLoaded()) {
    1559                 if (!t.hasError()) {
    1560                     result.hasVisibleTiles = true;
    1561                 }
    1562             } else if (t.isLoading()) {
    1563                 result.hasLoadingTiles = true;
    1564             }
    1565         }
    1566         return result;
    1567     }
    1568 
    1569     private class DeepTileSet {
    1570         private final ProjectionBounds bounds;
    1571         private final int minZoom, maxZoom;
    1572         private final TileSet[] tileSets;
    1573         private final TileSetInfo[] tileSetInfos;
    1574 
    1575         @SuppressWarnings("unchecked")
    1576         DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
    1577             this.bounds = bounds;
    1578             this.minZoom = minZoom;
    1579             this.maxZoom = maxZoom;
    1580             this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
    1581             this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
    1582         }
    1583 
    1584         public TileSet getTileSet(int zoom) {
    1585             if (zoom < minZoom)
    1586                 return nullTileSet;
    1587             synchronized (tileSets) {
    1588                 TileSet ts = tileSets[zoom-minZoom];
    1589                 if (ts == null) {
    1590                     ts = AbstractTileSourceLayer.this.getTileSet(bounds.getMin(), bounds.getMax(), zoom);
    1591                     tileSets[zoom-minZoom] = ts;
    1592                 }
    1593                 return ts;
    1594             }
    1595         }
    1596 
    1597         public TileSetInfo getTileSetInfo(int zoom) {
    1598             if (zoom < minZoom)
    1599                 return new TileSetInfo();
    1600             synchronized (tileSetInfos) {
    1601                 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    1602                 if (tsi == null) {
    1603                     tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
    1604                     tileSetInfos[zoom-minZoom] = tsi;
    1605                 }
    1606                 return tsi;
    1607             }
    1608         }
    1609     }
    1610 
    1611     @Override
    1612     public void paint(Graphics2D g, MapView mv, Bounds bounds) {
    1613         // old and unused.
    1614     }
    1615 
    1616     private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
    1617         int zoom = currentZoomLevel;
    1618         if (getDisplaySettings().isAutoZoom()) {
    1619             zoom = getBestZoom();
    1620         }
    1621 
    1622         DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
    1623         TileSet ts = dts.getTileSet(zoom);
    1624 
    1625         int displayZoomLevel = zoom;
    1626 
    1627         boolean noTilesAtZoom = false;
    1628         if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
    1629             // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
    1630             TileSetInfo tsi = dts.getTileSetInfo(zoom);
    1631             if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
    1632                 noTilesAtZoom = true;
    1633             }
    1634             // Find highest zoom level with at least one visible tile
    1635             for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
    1636                 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
    1637                     displayZoomLevel = tmpZoom;
    1638                     break;
    1639                 }
    1640             }
    1641             // Do binary search between currentZoomLevel and displayZoomLevel
    1642             while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
    1643                 zoom = (zoom + displayZoomLevel)/2;
    1644                 tsi = dts.getTileSetInfo(zoom);
    1645             }
    1646 
    1647             setZoomLevel(zoom);
    1648 
    1649             // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
    1650             // to make sure there're really no more zoom levels
    1651             // loading is done in the next if section
    1652             if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
    1653                 zoom++;
    1654                 tsi = dts.getTileSetInfo(zoom);
    1655             }
    1656             // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
    1657             // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
    1658             // loading is done in the next if section
    1659             while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
    1660                 zoom--;
    1661                 tsi = dts.getTileSetInfo(zoom);
    1662             }
    1663             ts = dts.getTileSet(zoom);
    1664         } else if (getDisplaySettings().isAutoZoom()) {
    1665             setZoomLevel(zoom);
    1666         }
    1667 
    1668         // Too many tiles... refuse to download
    1669         if (!ts.tooLarge()) {
    1670             //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
    1671             ts.loadAllTiles(false);
    1672         }
    1673 
    1674         if (displayZoomLevel != zoom) {
    1675             ts = dts.getTileSet(displayZoomLevel);
    1676         }
    1677 
    1678         g.setColor(Color.DARK_GRAY);
    1679 
    1680         List<Tile> missedTiles = this.paintTileImages(g, ts);
    1681         int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
    1682         for (int zoomOffset : otherZooms) {
    1683             if (!getDisplaySettings().isAutoZoom()) {
    1684                 break;
    1685             }
    1686             int newzoom = displayZoomLevel + zoomOffset;
    1687             if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
    1688                 continue;
    1689             }
    1690             if (missedTiles.isEmpty()) {
    1691                 break;
    1692             }
    1693             List<Tile> newlyMissedTiles = new LinkedList<>();
    1694             for (Tile missed : missedTiles) {
    1695                 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
    1696                     // Don't try to paint from higher zoom levels when tile is overzoomed
    1697                     newlyMissedTiles.add(missed);
    1698                     continue;
    1699                 }
    1700                 Tile t2 = tempCornerTile(missed);
    1701                 TileSet ts2 = getTileSet(
    1702                         getShiftedLatLon(tileSource.tileXYToLatLon(missed)),
    1703                         getShiftedLatLon(tileSource.tileXYToLatLon(t2)),
    1704                         newzoom);
    1705                 // Instantiating large TileSets is expensive.  If there
    1706                 // are no loaded tiles, don't bother even trying.
    1707                 if (ts2.allLoadedTiles().isEmpty()) {
    1708                     newlyMissedTiles.add(missed);
    1709                     continue;
    1710                 }
    1711                 if (ts2.tooLarge()) {
    1712                     continue;
    1713                 }
    1714                 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
    1715             }
    1716             missedTiles = newlyMissedTiles;
    1717         }
    1718         if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
    1719             Main.debug("still missed "+missedTiles.size()+" in the end");
    1720         }
    1721         g.setColor(Color.red);
    1722         g.setFont(InfoFont);
    1723 
    1724         // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
    1725         for (Tile t : ts.allExistingTiles()) {
    1726             this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
    1727         }
    1728 
    1729         EastNorth min = pb.getMin();
    1730         EastNorth max = pb.getMax();
    1731         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
    1732                 displayZoomLevel, this);
    1733 
    1734         //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
    1735         g.setColor(Color.lightGray);
    1736 
    1737         if (ts.insane()) {
    1738             myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
    1739         } else if (ts.tooLarge()) {
    1740             myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
    1741         } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
    1742             myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
    1743         }
    1744 
    1745         if (noTilesAtZoom) {
    1746             myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
    1747         }
    1748         if (Main.isDebugEnabled()) {
    1749             myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
    1750             myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
    1751             myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
    1752             myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
    1753             myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
    1754             if (tileLoader instanceof TMSCachedTileLoader) {
    1755                 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
    1756                 int offset = 200;
    1757                 for (String part: cachedTileLoader.getStats().split("\n")) {
    1758                     offset += 15;
    1759                     myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
    1760                 }
    1761             }
    1762         }
    1763     }
    1764 
    1765     /**
    1766      * Returns tile for a pixel position.<p>
    1767      * This isn't very efficient, but it is only used when the user right-clicks on the map.
    1768      * @param px pixel X coordinate
    1769      * @param py pixel Y coordinate
    1770      * @return Tile at pixel position
    1771      */
    1772     private Tile getTileForPixelpos(int px, int py) {
    1773         if (Main.isDebugEnabled()) {
    1774             Main.debug("getTileForPixelpos("+px+", "+py+')');
    1775         }
    1776         MapView mv = Main.map.mapView;
    1777         Point clicked = new Point(px, py);
    1778         EastNorth topLeft = mv.getEastNorth(0, 0);
    1779         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    1780         int z = currentZoomLevel;
    1781         TileSet ts = getTileSet(topLeft, botRight, z);
    1782 
    1783         if (!ts.tooLarge()) {
    1784             ts.loadAllTiles(false); // make sure there are tile objects for all tiles
    1785         }
    1786         Stream<Tile> clickedTiles = ts.allExistingTiles().stream()
    1787                 .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));
    1788         if (Main.isTraceEnabled()) {
    1789             clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +
    1790                     " currentZoomLevel: " + currentZoomLevel));
    1791         }
    1792         return clickedTiles.findAny().orElse(null);
    1793     }
    1794 
    1795529    @Override
    1796530    public Action[] getMenuEntries() {
    1797531        ArrayList<Action> actions = new ArrayList<>();
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1803537    }
    1804538
    1805539    public Action[] getLayerListEntries() {
    1806         return new Action[] {
    1807             LayerListDialog.getInstance().createActivateLayerAction(this),
    1808             LayerListDialog.getInstance().createShowHideLayerAction(),
    1809             LayerListDialog.getInstance().createDeleteLayerAction(),
    1810             SeparatorLayerAction.INSTANCE,
    1811             // color,
    1812             new OffsetAction(),
    1813             new RenameLayerAction(this.getAssociatedFile(), this),
    1814             SeparatorLayerAction.INSTANCE
    1815         };
     540        return new Action[] { LayerListDialog.getInstance().createActivateLayerAction(this),
     541                LayerListDialog.getInstance().createShowHideLayerAction(),
     542                LayerListDialog.getInstance().createDeleteLayerAction(), SeparatorLayerAction.INSTANCE,
     543                // color,
     544                new OffsetAction(), new RenameLayerAction(this.getAssociatedFile(), this),
     545                SeparatorLayerAction.INSTANCE };
    1816546    }
    1817547
    1818548    /**
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1821551     */
    1822552    public Action[] getCommonEntries() {
    1823553        return new Action[] {
    1824             new AutoLoadTilesAction(),
    1825             new AutoZoomAction(),
    1826             new ShowErrorsAction(),
    1827             new IncreaseZoomAction(),
    1828             new DecreaseZoomAction(),
    1829             new ZoomToBestAction(),
    1830             new ZoomToNativeLevelAction(),
    1831             new FlushTileCacheAction(),
    1832             new LoadErroneusTilesAction(),
    1833             new LoadAllTilesAction()
    1834         };
     554                new AutoLoadTilesAction(),
     555                new AutoZoomAction(),
     556                new ShowErrorsAction() };
     557    }
     558
     559    private List<Action> getMapViewEntries(MapView mv) {
     560        TileSourcePainter<T> painter = painters.get(mv);
     561        return painter.getMenuEntries();
    1835562    }
    1836563
    1837564    @Override
    1838565    public String getToolTipText() {
     566        String currentZoomLevel = painters.values().stream().findAny().map(TileSourcePainter::getZoomString).orElse("?");
    1839567        if (getDisplaySettings().isAutoLoad()) {
    1840             return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     568            return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(),
     569                    currentZoomLevel);
    1841570        } else {
    1842             return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     571            return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(),
     572                    currentZoomLevel);
    1843573        }
    1844574    }
    1845575
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1853583        return false;
    1854584    }
    1855585
     586    @Override
     587    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
     588        // never called, we use a custom painter
     589        throw new UnsupportedOperationException();
     590    }
     591
    1856592    /**
    1857593     * Task responsible for precaching imagery along the gpx track
    1858594     *
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1868604         */
    1869605        public PrecacheTask(ProgressMonitor progressMonitor) {
    1870606            this.progressMonitor = progressMonitor;
     607            // TODO
     608            T tileSource = null;
    1871609            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
    1872610            if (this.tileLoader instanceof TMSCachedTileLoader) {
    1873                 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
    1874                         TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
     611                ((TMSCachedTileLoader) this.tileLoader)
     612                        .setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
    1875613            }
    1876614        }
    1877615
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1929667     * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
    1930668     * @return precache task representing download task
    1931669     */
    1932     public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,
    1933             double bufferX, double bufferY) {
     670    public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor,
     671            List<LatLon> points, double bufferX, double bufferY) {
    1934672        PrecacheTask precacheTask = new PrecacheTask(progressMonitor);
    1935673        final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(
    1936674                (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
    1937         for (LatLon point: points) {
    1938 
     675        for (LatLon point : points) {
     676            //TODO
     677            TileSource tileSource = null;
     678            int currentZoomLevel = 0;
    1939679            TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
    1940680            TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
    1941681            TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1957697        precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
    1958698
    1959699        TileLoader loader = precacheTask.getTileLoader();
    1960         for (Tile t: requestedTiles) {
     700        for (Tile t : requestedTiles) {
    1961701            loader.createTileLoaderJob(t).submit();
    1962702        }
    1963703        return precacheTask;
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    1979719        adjustAction.destroy();
    1980720    }
    1981721
    1982     private class TileSourcePainter extends CompatibilityModeLayerPainter {
    1983         /**
    1984          * The memory handle that will hold our tile source.
    1985          */
    1986         private MemoryHandle<?> memory;
    1987 
    1988         @Override
    1989         public void paint(MapViewGraphics graphics) {
    1990             allocateCacheMemory();
    1991             if (memory != null) {
    1992                 doPaint(graphics);
    1993             }
    1994         }
    1995 
    1996         private void doPaint(MapViewGraphics graphics) {
    1997             ProjectionBounds pb = graphics.getClipBounds().getProjectionBounds();
    1998 
    1999             drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb);
    2000         }
    2001 
    2002         private void allocateCacheMemory() {
    2003             if (memory == null) {
    2004                 MemoryManager manager = MemoryManager.getInstance();
    2005                 if (manager.isAvailable(getEstimatedCacheSize())) {
    2006                     try {
    2007                         memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
    2008                     } catch (NotEnoughMemoryException e) {
    2009                         Main.warn("Could not allocate tile source memory", e);
    2010                     }
    2011                 }
    2012             }
    2013         }
    2014 
    2015         protected long getEstimatedCacheSize() {
    2016             return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
    2017         }
    2018 
    2019         @Override
    2020         public void detachFromMapView(MapViewEvent event) {
    2021             event.getMapView().removeMouseListener(adapter);
    2022             MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
    2023             super.detachFromMapView(event);
    2024             if (memory != null) {
    2025                 memory.free();
    2026             }
    2027         }
     722    /**
     723     * A {@link TileSourcePainter} notifies us of a dispatch
     724     * @param tileSourcePainter The painter.
     725     */
     726    public void detach(TileSourcePainter<T> tileSourcePainter) {
     727        GuiHelper.assertCallFromEdt();
     728        painters.entrySet().removeIf(e -> e.getValue().equals(tileSourcePainter));
    2028729    }
    2029730}
  • src/org/openstreetmap/josm/gui/layer/TMSLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
    index bf833ff..e220828 100644
    a b import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;  
    2222import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2323import org.openstreetmap.josm.data.preferences.IntegerProperty;
    2424import org.openstreetmap.josm.data.projection.Projection;
     25import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
     26import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter;
    2527
    2628/**
    2729 * Class that displays a slippy map layer.
    import org.openstreetmap.josm.data.projection.Projection;  
    3234 * @author Upliner &lt;upliner@gmail.com&gt;
    3335 * @since 3715
    3436 */
    35 public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> implements NativeScaleLayer {
     37public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> {
    3638    private static final String CACHE_REGION_NAME = "TMS";
    3739
    3840    private static final String PREFERENCE_PREFIX = "imagery.tms";
    public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple  
    5759        super(info);
    5860    }
    5961
    60     /**
    61      * Creates and returns a new TileSource instance depending on the {@link ImageryType}
    62      * of the passed ImageryInfo object.
    63      *
    64      * If no appropriate TileSource is found, null is returned.
    65      * Currently supported ImageryType are {@link ImageryType#TMS},
    66      * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
    67      *
    68      *
    69      * @param info imagery info
    70      * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
    71      * @throws IllegalArgumentException if url from imagery info is null or invalid
    72      */
    73     @Override
    74     protected TMSTileSource getTileSource(ImageryInfo info) {
    75         return getTileSourceStatic(info, () -> {
    76             Main.debug("Attribution loaded, running loadAllErrorTiles");
    77             this.loadAllErrorTiles(false);
    78         });
    79     }
    80 
    81     @Override
    82     public final boolean isProjectionSupported(Projection proj) {
    83         return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
    84     }
    85 
    8662    @Override
    8763    public final String nameSupportedProjections() {
    8864        return tr("EPSG:4326 and Mercator projection are supported");
    public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple  
    148124        return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
    149125    }
    150126
    151     @Override
    152     public ScaleList getNativeScales() {
    153         return nativeScaleList;
    154     }
    155127
    156128    private static ScaleList initNativeScaleList() {
    157129        Collection<Double> scales = new ArrayList<>(AbstractTileSourceLayer.MAX_ZOOM);
    public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple  
    161133        }
    162134        return new ScaleList(scales);
    163135    }
     136
     137    @Override
     138    protected TileSourcePainter<TMSTileSource> createMapViewPainter(MapViewEvent event) {
     139        return new TileSourcePainter<TMSTileSource>(this, event.getMapView()) {
     140            @Override
     141            public final boolean isProjectionSupported(Projection proj) {
     142                return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
     143            }
     144        //  TODO  @Override
     145//          public ScaleList getNativeScales() {
     146//              return nativeScaleList;
     147//          }
     148
     149            @Override
     150            protected TMSTileSource generateTileSource(AbstractTileSourceLayer<TMSTileSource> layer) {
     151                return getTileSourceStatic(info, () -> {
     152                    Main.debug("Attribution loaded, running loadAllErrorTiles");
     153                    loadAllErrorTiles(false);
     154                });
     155            }
     156        };
     157    }
    164158 }
  • src/org/openstreetmap/josm/gui/layer/WMSLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/WMSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
    index acf27c6..5c8fbf2 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    66import java.awt.event.ActionEvent;
    77import java.util.ArrayList;
    88import java.util.Arrays;
     9import java.util.HashSet;
    910import java.util.List;
    1011import java.util.Map;
    1112import java.util.Set;
    import java.util.TreeSet;  
    1314
    1415import javax.swing.AbstractAction;
    1516import javax.swing.Action;
    16 import javax.swing.JOptionPane;
    1717
    1818import org.apache.commons.jcs.access.CacheAccess;
    1919import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;  
    2727import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2828import org.openstreetmap.josm.data.preferences.IntegerProperty;
    2929import org.openstreetmap.josm.data.projection.Projection;
    30 import org.openstreetmap.josm.gui.ExtendedDialog;
     30import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
     31import org.openstreetmap.josm.gui.MapView;
    3132import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
     33import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter;
    3234
    3335/**
    3436 * This is a layer that grabs the current screen from an WMS server. The data
    public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour  
    115117    }
    116118
    117119    @Override
    118     public boolean isProjectionSupported(Projection proj) {
    119         return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode()) ||
    120                 (info.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")
    121                         && "EPSG:3857".equals(Main.getProjection().toCode()));
    122     }
    123 
    124     @Override
    125120    public String nameSupportedProjections() {
    126121        StringBuilder ret = new StringBuilder();
    127122        for (String e: supportedProjections) {
    public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour  
    136131        return ret.substring(0, ret.length()-2) + appendix;
    137132    }
    138133
    139     @Override
    140     public void projectionChanged(Projection oldValue, Projection newValue) {
    141         // do not call super - we need custom warning dialog
    142 
    143         if (!isProjectionSupported(newValue)) {
    144             String message =
    145                     "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) +
    146                     "<p style='width: 450px; position: absolute; margin: 0px;'>" +
    147                             tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
    148                     "<p>" + tr("Change the projection again or remove the layer.");
    149 
    150             ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}).
    151                     setContent(message).
    152                     setIcon(JOptionPane.WARNING_MESSAGE);
    153 
    154             if (isReprojectionPossible()) {
    155                 warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
    156             }
    157             warningDialog.showDialog();
    158         }
    159 
    160         if (!newValue.equals(oldValue)) {
    161             tileSource.initProjection(newValue);
    162         }
    163     }
     134//    @Override
     135//    public void projectionChanged(Projection oldValue, Projection newValue) {
     136//        // do not call super - we need custom warning dialog
     137//
     138//        if (!isProjectionSupported(newValue)) {
     139//            String message =
     140//                    "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) +
     141//                    "<p style='width: 450px; position: absolute; margin: 0px;'>" +
     142//                            tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
     143//                    "<p>" + tr("Change the projection again or remove the layer.");
     144//
     145//            ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}).
     146//                    setContent(message).
     147//                    setIcon(JOptionPane.WARNING_MESSAGE);
     148//
     149//            if (isReprojectionPossible()) {
     150//// TODO:               warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
     151//            }
     152//            warningDialog.showDialog();
     153//        }
     154//    }
    164155
    165156    @Override
    166157    protected Class<? extends TileLoader> getTileLoaderClass() {
    public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour  
    182173    private boolean isReprojectionPossible() {
    183174        return supportedProjections.contains("EPSG:4326") && "EPSG:3857".equals(Main.getProjection().toCode());
    184175    }
     176
     177    @Override
     178    protected TileSourcePainter<TemplatedWMSTileSource> createMapViewPainter(MapViewEvent event) {
     179        return new WMSPainter(this, event.getMapView());
     180    }
     181
     182    private static class WMSPainter extends TileSourcePainter<TemplatedWMSTileSource> {
     183        private final ProjectionChangeListener initOnProjectionChange = (oldValue, newValue) -> tileSource.initProjection(newValue);
     184        private HashSet<String> supportedProjections;
     185
     186        public WMSPainter(AbstractTileSourceLayer<TemplatedWMSTileSource> abstractTileSourceLayer, MapView mapView) {
     187            super(abstractTileSourceLayer, mapView);
     188            Main.addProjectionChangeListener(initOnProjectionChange);
     189
     190            ImageryInfo info2 = abstractTileSourceLayer.getInfo();
     191            supportedProjections = new HashSet<>(info2.getServerProjections());
     192            if (info2.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")) {
     193                supportedProjections.add("EPSG:3857");
     194            }
     195
     196            zoom.setZoomBounds(0, zoom.getMaxZoom());
     197        }
     198
     199        @Override
     200        public void detachFromMapView(MapViewEvent event) {
     201            Main.removeProjectionChangeListener(initOnProjectionChange);
     202            super.detachFromMapView(event);
     203        }
     204
     205        @Override
     206        public boolean isProjectionSupported(Projection proj) {
     207            return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode());
     208
     209        }
     210
     211    }
    185212}
  • src/org/openstreetmap/josm/gui/layer/WMTSLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
    index f4cb70c..07ac173 100644
    a b  
    22package org.openstreetmap.josm.gui.layer;
    33
    44import java.io.IOException;
    5 import java.util.Set;
    65
    76import org.apache.commons.jcs.access.CacheAccess;
    87import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;  
    1312import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
    1413import org.openstreetmap.josm.data.imagery.WMTSTileSource;
    1514import org.openstreetmap.josm.data.projection.Projection;
     15import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
     16import org.openstreetmap.josm.gui.MapView;
    1617import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
     18import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter;
    1719
    1820/**
    1921 * WMTS layer based on AbstractTileSourceLayer. Overrides few methods to align WMTS to Tile based computations
    import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;  
    2527 * @author Wiktor NiesiobÄ™dzki
    2628 *
    2729 */
    28 public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> implements NativeScaleLayer {
     30public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> {
    2931    private static final String PREFERENCE_PREFIX = "imagery.wmts";
    3032
    3133    /**
    public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> imp  
    5052        return new TileSourceDisplaySettings(PREFERENCE_PREFIX);
    5153    }
    5254
    53     @Override
    54     protected WMTSTileSource getTileSource(ImageryInfo info) {
    55         try {
    56             if (info.getImageryType() == ImageryType.WMTS && info.getUrl() != null) {
    57                 WMTSTileSource.checkUrl(info.getUrl());
    58                 WMTSTileSource tileSource = new WMTSTileSource(info);
    59                 info.setAttribution(tileSource);
    60                 return tileSource;
    61             }
    62             return null;
    63         } catch (IOException e) {
    64             Main.warn(e);
    65             throw new IllegalArgumentException(e);
    66         }
    67     }
    68 
    69     @Override
    70     protected int getBestZoom() {
    71         if (!Main.isDisplayingMapView())
    72             return 0;
    73         ScaleList scaleList = getNativeScales();
    74         if (scaleList == null) {
    75             return getMaxZoomLvl();
    76         }
    77         Scale snap = scaleList.getSnapScale(Main.map.mapView.getScale(), false);
    78         return Math.max(
    79                 getMinZoomLvl(),
    80                 Math.min(
    81                         snap != null ? snap.getIndex() : getMaxZoomLvl(),
    82                         getMaxZoomLvl()
    83                         )
    84                 );
    85     }
    86 
    87     @Override
    88     protected int getMinZoomLvl() {
    89         return 0;
    90     }
    91 
    92     @Override
    93     public boolean isProjectionSupported(Projection proj) {
    94         Set<String> supportedProjections = tileSource.getSupportedProjections();
    95         return supportedProjections.contains(proj.toCode());
    96     }
    97 
    98     @Override
    99     public String nameSupportedProjections() {
    100         StringBuilder ret = new StringBuilder();
    101         for (String e: tileSource.getSupportedProjections()) {
    102             ret.append(e).append(", ");
    103         }
    104         return ret.length() > 2 ? ret.substring(0, ret.length()-2) : ret.toString();
    105     }
    106 
    107     @Override
    108     public void projectionChanged(Projection oldValue, Projection newValue) {
    109         super.projectionChanged(oldValue, newValue);
    110         tileSource.initProjection(newValue);
    111     }
     55    //  TODO  @Override
     56    //    protected int getBestZoom() {
     57    //        if (!Main.isDisplayingMapView())
     58    //            return 0;
     59    //        ScaleList scaleList = getNativeScales();
     60    //        if (scaleList == null) {
     61    //            return getMaxZoomLvl();
     62    //        }
     63    //        Scale snap = scaleList.getSnapScale(Main.map.mapView.getScale(), false);
     64    //        return Math.max(
     65    //                getMinZoomLvl(),
     66    //                Math.min(
     67    //                        snap != null ? snap.getIndex() : getMaxZoomLvl(),
     68    //                        getMaxZoomLvl()
     69    //                        )
     70    //                );
     71    //    }
     72
     73    //
     74    //    @Override
     75    //    public String nameSupportedProjections() {
     76    //        StringBuilder ret = new StringBuilder();
     77    //        for (String e: tileSource.getSupportedProjections()) {
     78    //            ret.append(e).append(", ");
     79    //        }
     80    //        return ret.length() > 2 ? ret.substring(0, ret.length()-2) : ret.toString();
     81    //    }
    11282
    11383    @Override
    11484    protected Class<? extends TileLoader> getTileLoaderClass() {
    public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> imp  
    12797        return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
    12898    }
    12999
     100    //  TODO  @Override
     101    //    public ScaleList getNativeScales() {
     102    //        return tileSource.getNativeScales();
     103    //    }
     104
    130105    @Override
    131     public ScaleList getNativeScales() {
    132         return tileSource.getNativeScales();
     106    protected TileSourcePainter<WMTSTileSource> createMapViewPainter(MapViewEvent event) {
     107        return new WMTSPainter(this, event.getMapView());
     108    }
     109
     110    private static class WMTSPainter extends TileSourcePainter<WMTSTileSource> {
     111        private final ProjectionChangeListener initOnProjectionChange = (oldValue, newValue) -> tileSource
     112                .initProjection(newValue);
     113
     114        public WMTSPainter(AbstractTileSourceLayer<WMTSTileSource> abstractTileSourceLayer, MapView mapView) {
     115            super(abstractTileSourceLayer, mapView);
     116            Main.addProjectionChangeListener(initOnProjectionChange);
     117
     118            zoom.setZoomBounds(0, zoom.getMaxZoom());
     119        }
     120
     121        @Override
     122        protected WMTSTileSource generateTileSource(AbstractTileSourceLayer<WMTSTileSource> layer) {
     123            try {
     124                ImageryInfo layerInfo = layer.getInfo();
     125                if (layerInfo.getImageryType() == ImageryType.WMTS && layerInfo.getUrl() != null) {
     126                    WMTSTileSource.checkUrl(layerInfo.getUrl());
     127                    WMTSTileSource tileSource = new WMTSTileSource(layerInfo);
     128                    layerInfo.setAttribution(tileSource);
     129                    return tileSource;
     130                }
     131                return null;
     132            } catch (IOException e) {
     133                Main.warn(e);
     134                throw new IllegalArgumentException(e);
     135            }
     136        }
     137
     138        @Override
     139        public void detachFromMapView(MapViewEvent event) {
     140            Main.removeProjectionChangeListener(initOnProjectionChange);
     141            super.detachFromMapView(event);
     142        }
     143
     144        @Override
     145        public boolean isProjectionSupported(Projection proj) {
     146            return tileSource.getSupportedProjections().contains(proj.toCode());
     147        }
    133148    }
    134149}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java b/src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java
    new file mode 100644
    index 0000000..248cded
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Dimension;
     7import java.awt.event.ActionEvent;
     8import java.util.function.Predicate;
     9
     10import javax.swing.AbstractAction;
     11
     12import org.openstreetmap.gui.jmapviewer.AttributionSupport;
     13import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
     14import org.openstreetmap.gui.jmapviewer.Tile;
     15import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     16import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     17import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     18import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
     19import org.openstreetmap.josm.Main;
     20import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     21import org.openstreetmap.josm.gui.MapView;
     22import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
     23import org.openstreetmap.josm.gui.PleaseWaitRunnable;
     24import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
     25import org.openstreetmap.josm.gui.layer.imagery.TileForAreaFinder.TileForAreaGetter;
     26import org.openstreetmap.josm.gui.util.GuiHelper;
     27import org.openstreetmap.josm.tools.MemoryManager;
     28import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
     29import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
     30
     31/**
     32 * This class backs the {@link TileSourcePainter} by handling the loading / acces of the tile images
     33 * @author Michael Zangl
     34 * @param <T> The imagery type to use
     35 * @since xxx
     36 */
     37public abstract class AbstractTileSourceLoader<T extends AbstractTMSTileSource> implements TileForAreaGetter, ZoomChangeListener {
     38
     39    /*
     40     *  use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image)
     41     *  and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible
     42     *  in MapView (for example - when limiting min zoom in imagery)
     43     *
     44     *  Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached
     45     */
     46    private final TileCache tileCache; // initialized together with tileSource
     47    protected final T tileSource;
     48    protected final TileLoader tileLoader;
     49    protected final AttributionSupport attribution = new AttributionSupport();
     50
     51    /**
     52     * The memory handle that will hold our tile source.
     53     */
     54    private MemoryHandle<?> memory;
     55
     56    protected AbstractTileSourceLoader(AbstractTileSourceLayer<T> layer) {
     57        tileSource = generateTileSource(layer);
     58        if (tileSource == null) {
     59            throw new IllegalArgumentException(tr("Failed to create tile source"));
     60        }
     61
     62        attribution.initialize(tileSource);
     63
     64        tileLoader = layer.generateTileLoader(tileSource);
     65
     66        tileCache = new MemoryTileCache(estimateTileCacheSize());
     67        MapView.addZoomChangeListener(this);
     68    }
     69
     70    protected T generateTileSource(AbstractTileSourceLayer<T> layer) {
     71        return layer.getTileSource();
     72    }
     73    /**
     74     * Check if there are any matching tiles in the given range
     75     * @param range The range to check in
     76     * @param pred The predicate the tiles need to match
     77     * @return If there are such tiles.
     78     */
     79    public boolean hasTiles(TileRange range, Predicate<Tile> pred) {
     80        return range.tilePositions().map(this::getTile).anyMatch(pred);
     81    }
     82
     83    protected Tile getOrCreateTile(TilePosition tilePosition) {
     84        Tile tile = getTile(tilePosition);
     85        if (tile == null) {
     86            tile = new Tile(tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
     87            tileCache.addTile(tile);
     88        }
     89
     90        if (!tile.isLoaded()) {
     91            tile.loadPlaceholderFromCache(tileCache);
     92        }
     93        return tile;
     94    }
     95
     96    /**
     97     * Returns tile at given position.
     98     * This can and will return null for tiles that are not already in the cache.
     99     * @param tilePosition The position
     100     * @return tile at given position
     101     */
     102    protected Tile getTile(TilePosition tilePosition) {
     103        if (!contains(tilePosition)) {
     104            return null;
     105        } else {
     106            return tileCache.getTile(tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
     107        }
     108    }
     109
     110    /**
     111     * Check if this tile source contains the given position.
     112     * @param position The position
     113     * @return <code>true</code> if that positon is contained.
     114     */
     115    private boolean contains(TilePosition position) {
     116        return position.getZoom() >= tileSource.getMinZoom() && position.getZoom() <= tileSource.getMaxZoom()
     117                && position.getX() >= tileSource.getTileXMin(position.getZoom())
     118                && position.getX() <= tileSource.getTileXMax(position.getZoom())
     119                && position.getY() >= tileSource.getTileYMin(position.getZoom())
     120                && position.getY() <= tileSource.getTileYMax(position.getZoom());
     121    }
     122
     123    protected void loadTiles(TileRange range, boolean force) {
     124        if (force) {
     125            if (isTooLarge(range)) {
     126                Main.warn("Not downloading all tiles because there are too many tiles on an axis!");
     127            } else {
     128                range.tilePositionsSorted().filter(this::contains).forEach(t -> loadTile(t, force));
     129            }
     130        }
     131    }
     132
     133    protected static boolean isTooSmall(TileRange range) {
     134        return range.tilesSpanned() < 2;
     135    }
     136
     137    protected boolean isTooLarge(TileRange range) {
     138        return range.size() > tileCache.getCacheSize() || range.tilesSpanned() > 20;
     139    }
     140
     141    protected boolean loadTile(TilePosition tile, boolean force) {
     142        return loadTile(getOrCreateTile(tile), force);
     143    }
     144
     145    private boolean loadTile(Tile tile, boolean force) {
     146        if (tile == null)
     147            return false;
     148        if (!force && (tile.isLoaded() || tile.hasError() || isOverzoomed(tile)))
     149            return false;
     150        if (tile.isLoading())
     151            return false;
     152        tileLoader.createTileLoaderJob(tile).submit(force);
     153        return true;
     154    }
     155
     156    @Override
     157    public void zoomChanged() {
     158        if (tileLoader instanceof TMSCachedTileLoader) {
     159            ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
     160        }
     161    }
     162
     163    /**
     164     * Test if a tile is visible.
     165     * @param t The tile to test
     166     * @return <code>true</code> if it is visible
     167     */
     168    public static boolean isVisible(Tile t) {
     169        return t != null && t.isLoaded() && !t.hasError();
     170    }
     171
     172    /**
     173     * Test if a tile is missing.
     174     * @param t The tile to test
     175     * @return <code>true</code> if it is loading or not loaded yet.
     176     */
     177    public static boolean isMissing(Tile t) {
     178        return t == null || t.isLoading();
     179    }
     180
     181    /**
     182     * Test if a tile is marked as loading.
     183     * @param t The tile to test
     184     * @return <code>true</code> if it is loading
     185     */
     186    public static boolean isLoading(Tile t) {
     187        return t != null && t.isLoading();
     188    }
     189
     190    /**
     191     * Test if a tile is marked as overzoomed.
     192     * @param t The tile to test
     193     * @return <code>true</code> if it is overzoomed
     194     */
     195    public static boolean isOverzoomed(Tile t) {
     196        return t != null && "no-tile".equals(t.getValue("tile-info"));
     197    }
     198
     199    /**
     200     * Reserve the memory for the cache
     201     * @return <code>true</code> if it is reserved.
     202     */
     203    protected boolean allocateCacheMemory() {
     204        if (memory == null) {
     205            MemoryManager manager = MemoryManager.getInstance();
     206            if (manager.isAvailable(getEstimatedCacheSize())) {
     207                try {
     208                    memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
     209                } catch (NotEnoughMemoryException e) {
     210                    Main.warn("Could not allocate tile source memory", e);
     211                }
     212            }
     213        }
     214        return memory != null;
     215    }
     216
     217    /**
     218     * Free the cache memeory
     219     */
     220    protected void freeCacheMemory() {
     221        if (memory != null) {
     222            memory.free();
     223        }
     224    }
     225
     226    protected long getEstimatedCacheSize() {
     227        return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
     228    }
     229
     230    protected int estimateTileCacheSize() {
     231        Dimension screenSize = GuiHelper.getMaximumScreenSize();
     232        int height = screenSize.height;
     233        int width = screenSize.width;
     234        int tileSize = 256; // default tile size
     235        if (tileSource != null) {
     236            tileSize = tileSource.getTileSize();
     237        }
     238        // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
     239        int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1)
     240                * Math.ceil((double) width / tileSize + 1));
     241        // add 10% for tiles from different zoom levels
     242        // use offset to decide, how many tiles are visible
     243        int ret = (int) Math.ceil(Math.pow(2d, AbstractTileSourceLayer.ZOOM_OFFSET.get()) * visibileTiles * 4);
     244        Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles,
     245                ret);
     246        return ret;
     247    }
     248
     249
     250    protected class FlushTileCacheAction extends AbstractAction {
     251        FlushTileCacheAction() {
     252            super(tr("Flush tile cache"));
     253            setEnabled(tileLoader instanceof CachedTileLoader);
     254        }
     255
     256        @Override
     257        public void actionPerformed(ActionEvent ae) {
     258            new PleaseWaitRunnable(tr("Flush tile cache")) {
     259                @Override
     260                protected void realRun() {
     261                    clearTileCache();
     262                }
     263
     264                @Override
     265                protected void finish() {
     266                    // empty - flush is instaneus
     267                }
     268
     269                @Override
     270                protected void cancel() {
     271                    // empty - flush is instaneus
     272                }
     273
     274                /**
     275                 * Clears the tile cache.
     276                 */
     277                private void clearTileCache() {
     278                    if (tileLoader instanceof CachedTileLoader) {
     279                        ((CachedTileLoader) tileLoader).clearCache(tileSource);
     280                    }
     281                    tileCache.clear();
     282                }
     283            }.run();
     284        }
     285    }
     286}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java
    new file mode 100644
    index 0000000..580758e
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import java.awt.Color;
     5import java.awt.Graphics2D;
     6import java.awt.geom.AffineTransform;
     7
     8/**
     9 * This class handles text painting on the tile source layer.
     10 * @author Michael Zangl
     11 * @since xxx
     12 */
     13public class TextPainter {
     14    private Graphics2D g;
     15    private int debugY;
     16    private int overlayY;
     17
     18    /**
     19     * Reset internal state and start.
     20     * @param g The graphics to paint on
     21     */
     22    public void start(Graphics2D g) {
     23        this.g = g;
     24        debugY = 160;
     25        overlayY = 100;
     26    }
     27
     28    /**
     29     * Add a debug string
     30     * @param debugLine The debug line
     31     */
     32    public void addDebug(String debugLine) {
     33        debugY += drawString(debugLine, 50, debugY, 500);
     34    }
     35
     36    /**
     37     * Draw a string onto a given tile.
     38     * @param text The text to draw
     39     * @param tile The tile to paint on
     40     * @param converter A converter to convert the tile to screen coordinates.
     41     */
     42    public void drawTileString(String text, TilePosition tile, TileCoordinateConverter converter) {
     43        AffineTransform transform = converter.getTransformForTile(tile, 0, 0, 0, .5, 1, .5);
     44        transform.scale(1.0 / 200, 1.0 / 200);
     45        AffineTransform oldTransform = g.getTransform();
     46        g.transform(transform);
     47        drawString(text, 10, 10, 180);
     48        g.setTransform(oldTransform);
     49    }
     50
     51    /**
     52     * Add a text overlay for the map.
     53     * @param text The text to add.
     54     */
     55    public void addTextOverlay(String text) {
     56        overlayY += drawString(text, 120, overlayY, 500);
     57    }
     58
     59    private int drawString(String text, int x, int y, int width) {
     60        String textToDraw = text;
     61        int maxLineWidth = 0;
     62        int wholeLineWidth = g.getFontMetrics().stringWidth(text);
     63        if (wholeLineWidth > width) {
     64            // text longer than tile size, split it
     65            StringBuilder line = new StringBuilder();
     66            StringBuilder ret = new StringBuilder();
     67            for (String s: text.split(" ")) {
     68                int lineWidth = g.getFontMetrics().stringWidth(line.toString() + s);
     69                if (lineWidth > width) {
     70                    ret.append(line).append('\n');
     71                    line.setLength(0);
     72                    lineWidth = g.getFontMetrics().stringWidth(s);
     73                }
     74                line.append(s).append(' ');
     75                maxLineWidth = Math.max(lineWidth, maxLineWidth);
     76            }
     77            ret.append(line);
     78            textToDraw = ret.toString();
     79        } else {
     80            maxLineWidth = wholeLineWidth;
     81        }
     82
     83        return drawLines(x, y, textToDraw.split("\n"), maxLineWidth);
     84    }
     85
     86    private int drawLines(int x, int y, String[] lines, int maxLineWidth) {
     87        int height = g.getFontMetrics().getHeight();
     88
     89        // background
     90        g.setColor(new Color(0, 0, 0, 50));
     91        g.fillRect(x - 3, y - height - 1, maxLineWidth + 6, (3 + height) * lines.length + 2);
     92
     93        int offset = 0;
     94        for (String s: lines) {
     95            // shadow
     96            g.setColor(Color.black);
     97            g.drawString(s, x + 1, y + offset + 1);
     98            g.setColor(Color.lightGray);
     99            g.drawString(s, x, y + offset);
     100            offset += height + 3;
     101        }
     102        return offset;
     103    }
     104
     105}
  • src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
    index ff368e5..fc76dce 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.gui.layer.imagery;
    33
     4import java.awt.Shape;
     5import java.awt.geom.AffineTransform;
    46import java.awt.geom.Point2D;
    5 import java.awt.geom.Rectangle2D;
    67
    78import org.openstreetmap.gui.jmapviewer.Tile;
    89import org.openstreetmap.gui.jmapviewer.TileXY;
    910import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    1011import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     12import org.openstreetmap.josm.data.Bounds;
    1113import org.openstreetmap.josm.data.coor.LatLon;
    12 import org.openstreetmap.josm.data.projection.Projecting;
    13 import org.openstreetmap.josm.data.projection.ShiftedProjecting;
    14 import org.openstreetmap.josm.gui.MapView;
     14import org.openstreetmap.josm.gui.MapViewState;
     15import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle;
    1516import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     17import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
     18import org.openstreetmap.josm.tools.Utils;
    1619
    1720/**
    1821 * This class handles tile coordinate management and computes their position in the map view.
    import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;  
    2023 * @since 10651
    2124 */
    2225public class TileCoordinateConverter {
    23     private MapView mapView;
    24     private TileSourceDisplaySettings settings;
     26    private MapViewState displacedState;
    2527    private TileSource tileSource;
    2628
    2729    /**
    public class TileCoordinateConverter {  
    3032     * @param tileSource The tile source to use when converting coordinates.
    3133     * @param settings displacement settings.
    3234     */
    33     public TileCoordinateConverter(MapView mapView, TileSource tileSource, TileSourceDisplaySettings settings) {
    34         this.mapView = mapView;
     35    public TileCoordinateConverter(MapViewState mapView, TileSource tileSource, TileSourceDisplaySettings settings) {
     36        this.displacedState = mapView.shifted(settings.getDisplacement());
    3537        this.tileSource = tileSource;
    36         this.settings = settings;
    3738    }
    3839
    39     private MapViewPoint pos(ICoordinate ll) {
    40         return mapView.getState().getPointFor(new LatLon(ll)).add(settings.getDisplacement());
    41     }
    42 
    43     /**
    44      * Gets the projecting instance to use to convert between latlon and eastnorth coordinates.
    45      * @return The {@link Projecting} instance.
    46      */
    47     public Projecting getProjecting() {
    48         return new ShiftedProjecting(mapView.getProjection(), settings.getDisplacement());
     40    protected MapViewPoint pos(ICoordinate ll) {
     41        return displacedState.getPointFor(new LatLon(ll));
    4942    }
    5043
    5144    /**
    public class TileCoordinateConverter {  
    6356     * @param tile The tile
    6457     * @return The positon.
    6558     */
    66     public Rectangle2D getRectangleForTile(Tile tile) {
    67         ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
    68         ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     59    public MapViewLatLonRectangle getAreaForTile(TilePosition tile) {
     60        MapViewPoint p1 = tileUV(tile, 0, 0);
     61        MapViewPoint p2 = tileUV(tile, 1, 1);
    6962
    70         return pos(c1).rectTo(pos(c2)).getInView();
     63        return p1.latLonRectTo(p2);
     64    }
     65
     66    /**
     67     * Gets an affine transform that maps image u/v (0..1) space to east/north space.
     68     * <p>
     69     * You need to scale it by the image size to draw the buffered image.
     70     * @param tile
     71     * @param u1
     72     * @param v1
     73     * @param u2
     74     * @param v2
     75     * @param u3
     76     * @param v3
     77     * @return the transform
     78     */
     79    public AffineTransform getTransformForTile(TilePosition tile, double u1, double v1, double u2, double v2, double u3, double v3) {
     80        MapViewPoint p1 = tileUV(tile, u1, v1);
     81        MapViewPoint p2 = tileUV(tile, u2, v2);
     82        MapViewPoint p3 = tileUV(tile, u3, v3);
     83
     84        // We compute the matrix in a way that p_i.inView is mapped to the corresponding image position.
     85        // ( u1 )   ( m00 m01 m02  )   (p1.viewX )
     86        // ( v1 ) * ( m10 m11 m12  ) = (p1.viewY )
     87        // ( 1  )   (  0   0   1   )   (1        )
     88        // ( u2 )   ( m00 m01 m02  )   (p2.viewX )
     89        // ( v2 ) * ( m10 m11 m12  ) = (p2.viewY )
     90        // ( 1  )   (  0   0   1   )   (1        )
     91        // ( u3 )   ( m00 m01 m02  )   (p3.viewX )
     92        // ( v3 ) * ( m10 m11 m12  ) = (p3.viewY )
     93        // ( 1  )   (  0   0   1   )   (1        )
     94
     95        // u1 * m00 + v1 * m01 + m02 = p1.viewX
     96        // u2 * m00 + v2 * m01 + m02 = p2.viewX
     97        // u3 * m00 + v3 * m01 + m02 = p3.viewX
     98        // u1 * m10 + v1 * m11 + m12 = p1.viewY
     99        // u2 * m10 + v2 * m11 + m12 = p2.viewY
     100        // u3 * m10 + v3 * m11 + m12 = p3.viewY
     101
     102        // u1        * m00 + v1        * m01 + m02 = p1.viewX
     103        // (u2 - u1) * m00 + (v2 - v1) * m01       = p2.viewX - p1.viewX
     104        // (u3 - u1) * m00 + (v3 - v1) * m01       = p3.viewX - p1.viewX
     105
     106        // if v2 != v1 and v3 != v1
     107        // (u2 - u1) / (v2 - v1) * m00 + m01       = (p2.viewX - p1.viewX) / (v2 - v1)
     108        // (u3 - u1) / (v3 - v1) * m00 + m01       = (p3.viewX - p1.viewX) / (v3 - v1)
     109
     110        // m00 = ((p2.viewX - p1.viewX) / (v2 - v1) - (p3.viewX - p1.viewX) / (v3 - v1)) / ((u2 - u1) / (v2 - v1) - (u3 - u1) / (v3 - v1))
     111        // m01 = (p3.viewX - p1.viewX) / (v3 - v1) - (u3 - u1) / (v3 - v1) * m00
     112        // m02 = p1.viewX - u1 * m00 + v1 * m01
     113
     114        // if v2 == v1:
     115        // u1        * m00 + v1        * m01 + m02 = p1.viewX
     116        // (u2 - u1) * m00 +                       = p2.viewX - p1.viewX
     117        // (u3 - u1) * m00 + (v3 - v1) * m01       = p3.viewX - p1.viewX
     118
     119        // if v3 == v1
     120        // u1        * m00 + v1        * m01 + m02 = p1.viewX
     121        // (u2 - u1) * m00 + (v2 - v1) * m01       = p2.viewX - p1.viewX
     122        // (u3 - u1) * m00 +                       = p3.viewX - p1.viewX
     123
     124
     125        double du2 = u2 - u1;
     126        double du3 = u3 - u1;
     127        double dv2 = v2 - v1;
     128        double dv3 = v3 - v1;
     129
     130        // x space
     131        double p1x = p1.getInView().getX();
     132        double p2x = p2.getInView().getX();
     133        double p3x = p3.getInView().getX();
     134
     135        double m00;
     136        double m01;
     137        if (Utils.equalsEpsilon(0, dv2)) {
     138            if (Utils.equalsEpsilon(0, du2) || Utils.equalsEpsilon(0, dv3)) {
     139                // unsolveable
     140                return new AffineTransform();
     141            }
     142            m00 = (p2x - p1x) / du2;
     143            m01 = (p3x - p1x) / dv3 - du3 / dv3 * m00;
     144       } else if (Utils.equalsEpsilon(0, dv3)) {
     145            if (Utils.equalsEpsilon(0, du3)) {
     146                // unsolveable
     147                return new AffineTransform();
     148            }
     149            m00 = (p3x - p1x) / du3;
     150            m01 = (p2x - p1x) / dv2 - du2 / dv2 * m00;
     151        } else {
     152            m00 = ((p2x - p1x) / dv2 - (p3x - p1x) / dv3) / (du2 / dv2 - du3 / dv3);
     153            m01 = (p3x - p1x) / dv3 - du3 / dv3 * m00;
     154        }
     155        double m02 = p1x - u1 * m00 + v1 * m01;
     156
     157        // y space
     158        double p1y = p1.getInView().getY();
     159        double p2y = p2.getInView().getY();
     160        double p3y = p3.getInView().getY();
     161        double m10;
     162        double m11;
     163        if (Utils.equalsEpsilon(0, dv2)) {
     164            m10 = (p2y - p1y) / du2;
     165            m11 = (p3y - p1y) / dv3 - du3 / dv3 * m10;
     166       } else if (Utils.equalsEpsilon(0, dv3)) {
     167            m10 = (p3y - p1y) / du3;
     168            m11 = (p2y - p1y) / dv2 - du2 / dv2 * m10;
     169        } else {
     170            m10 = ((p2y - p1y) / dv2 - (p3y - p1y) / dv3) / (du2 / dv2 - du3 / dv3);
     171            m11 = (p3y - p1y) / dv3 - du3 / dv3 * m10;
     172        }
     173        double m12 = p1y - u1 * m10 + v1 * m11;
     174
     175        return new AffineTransform(new double[] {
     176                m00, m10, m01, m11, m02, m12
     177        });
     178    }
     179
     180    private MapViewPoint tileUV(TilePosition tile, double u, double v) {
     181        ICoordinate tileLatLon = tileSource.tileXYToLatLon(tile.getX(), tile.getY(), tile.getZoom());
     182        if (Utils.equalsEpsilon(0, u) && Utils.equalsEpsilon(0, v)) {
     183            return pos(tileLatLon);
     184        } else {
     185            ICoordinate nextTile = tileSource.tileXYToLatLon(tile.getX() + 1, tile.getY() + 1, tile.getZoom());
     186            return displacedState.getPointFor(new LatLon(
     187                    (1 - v) * tileLatLon.getLat() + v * nextTile.getLat(),
     188                    (1 - u) * tileLatLon.getLon() + u * nextTile.getLon()));
     189        }
    71190    }
    72191
    73192    /**
    public class TileCoordinateConverter {  
    76195     * @return average number of screen pixels per tile pixel
    77196     */
    78197    public double getScaleFactor(int zoom) {
    79         LatLon topLeft = mapView.getLatLon(0, 0);
    80         LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
    81         TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
    82         TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
    83 
    84         int screenPixels = mapView.getWidth()*mapView.getHeight();
    85         double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
    86         if (screenPixels == 0 || tilePixels == 0) return 1;
    87         return screenPixels/tilePixels;
     198        Bounds area = displacedState.getViewArea().getCornerBounds();
     199        TileXY t1 = tileSource.latLonToTileXY(area.getMin().toCoordinate(), zoom);
     200        TileXY t2 = tileSource.latLonToTileXY(area.getMax().toCoordinate(), zoom);
     201
     202        double screenPixels = displacedState.getViewWidth() * displacedState.getViewHeight();
     203        int tileSize = tileSource.getTileSize();
     204        double tilePixels = Math.abs((t2.getY() - t1.getY()) * (t2.getX() - t1.getX()) * tileSize * tileSize);
     205        if (screenPixels < 1e-10 || tilePixels < 1e-10) {
     206            return 1;
     207        } else {
     208            return screenPixels / tilePixels;
     209        }
     210    }
     211
     212    /**
     213     * Get the tiles in view at the given zoom level.
     214     * @param zoom The zoom level
     215     * @return The tiles that are in the view.
     216     */
     217    public TileRange getViewAtZoom(int zoom) {
     218        Bounds view = displacedState.getViewArea().getLatLonBoundsBox();
     219        view = view.intersect(displacedState.getProjection().getWorldBoundsLatLon());
     220        if (view == null) {
     221            return new TileRange();
     222        } else {
     223            TileXY t1 = tileSource.latLonToTileXY(view.getMin().toCoordinate(), zoom);
     224            TileXY t2 = tileSource.latLonToTileXY(view.getMax().toCoordinate(), zoom);
     225            return new TileRange(t1, t2, zoom);
     226        }
     227    }
     228
     229    /**
     230     * Gets the mathematically best zoom. May be out of range.
     231     * @return The zoom
     232     */
     233    public int getBestZoom() {
     234        double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
     235        double result = Math.log(factor) / Math.log(2) / 2;
     236        /*
     237         * Math.log(factor)/Math.log(2) - gives log base 2 of factor
     238         * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
     239         *
     240         * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET
     241         * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET
     242         * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or
     243         * maps as a imagery layer
     244         */
     245
     246        return (int) Math.round(result + 1 + AbstractTileSourceLayer.ZOOM_OFFSET.get() / 1.9);
     247    }
     248
     249    /**
     250     * Gets the clip to use to only paint inside the projection
     251     * @return The clip.
     252     */
     253    public Shape getProjectionClip() {
     254        return displacedState.getArea(displacedState.getProjection().getWorldBoundsLatLon());
    88255    }
    89256}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java
    new file mode 100644
    index 0000000..3410390
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import java.util.ArrayList;
     5import java.util.Collections;
     6import java.util.List;
     7import java.util.stream.Collectors;
     8import java.util.stream.Stream;
     9
     10import org.openstreetmap.josm.Main;
     11import org.openstreetmap.josm.data.Bounds;
     12
     13/**
     14 * This class helps finding loaded tiles for a tile area.
     15 * @author Michael Zangl
     16 * @since xxx
     17 */
     18public final class TileForAreaFinder {
     19
     20    private TileForAreaFinder() {
     21        // hidden
     22    }
     23
     24    /**
     25     * Get a stream of all tile positions to paint for the given zoom level.
     26     * @param initialRange The range
     27     * @param rangeProducer An object that converts between {@link Bounds} and {@link TilePosition}
     28     * @return A stream of tiles to paint.
     29     */
     30    public static Stream<TilePosition> getAtDefaultZoom(TileRange initialRange, TileForAreaGetter rangeProducer) {
     31        return initialRange.tilePositions().filter(rangeProducer::isAvailable);
     32    }
     33
     34    /**
     35     * Gets a stream of all positions to be painted taking the fallback zoom levels into account.
     36     * <p>
     37     * Limiting the resulting stream won't change the performance of this method. It returns a stream so that this may be changed in the future.
     38     *
     39     * @param initialRange The range
     40     * @param rangeProducer An object that converts between {@link Bounds} and {@link TilePosition}
     41     * @param zoom The zoom levels to try at.
     42     * @return A stream of tiles to paint.
     43     */
     44    public static Stream<TilePosition> getWithFallbackZoom(TileRange initialRange, TileForAreaGetter rangeProducer, ZoomLevelManager zoom) {
     45        ArrayList<TilePosition> list = new ArrayList<>();
     46        List<List<Bounds>> missedInPreviousRuns = new ArrayList<>();
     47        List<Bounds> missed = initialRange.tilePositions().flatMap(pos -> addPosition(pos, rangeProducer, list)).collect(Collectors.toList());
     48        List<Bounds> missedInLastRun = missed;
     49
     50        for (int delta : new int[] { -1, 1, -2, 2, -3, -4, -5 }) {
     51            int zoomLevel = delta + initialRange.getZoom();
     52            if (zoomLevel >= zoom.getMinZoom() && zoomLevel <= zoom.getMaxZoom()) {
     53                missed = missedInLastRun
     54                    .stream()
     55                    .flatMap(b -> rangeProducer.toRangeAtZoom(b, zoomLevel).tilePositions())
     56                    .distinct()
     57                    .filter(tile -> missedInPreviousRuns.stream().allMatch(l -> l.stream().anyMatch(rangeProducer.getBounds(tile)::intersects)))
     58                    .flatMap(pos -> addPosition(pos, rangeProducer, list))
     59                    .collect(Collectors.toList());
     60                Main.trace("Still missed {0} tile areas at zoom {1}.", missed.size(), zoomLevel);
     61                if (missed.isEmpty()) {
     62                    break;
     63                }
     64
     65                missedInPreviousRuns.add(missedInLastRun);
     66                missedInLastRun = missed;
     67            }
     68            // no break condition. But missed will be empty, so flatMap should not be costy.
     69        }
     70
     71        Collections.reverse(list);
     72        return list.stream().distinct();
     73    }
     74
     75    private static Stream<Bounds> addPosition(TilePosition pos, TileForAreaGetter rangeProducer, ArrayList<TilePosition> addTo) {
     76        if (rangeProducer.isAvailable(pos)) {
     77            addTo.add(pos);
     78            return Stream.empty();
     79        } else {
     80            return Stream.of(rangeProducer.getBounds(pos));
     81        }
     82    }
     83
     84    /**
     85     * Classes implementing this interface allow us to convert between a tile range and {@link Bounds}.
     86     * @author Michael Zangl
     87     * @since xxx
     88     */
     89    public interface TileForAreaGetter {
     90        /**
     91         * Gets a tile range that is enclosing this tile at the given zoom level.
     92         * @param bounds The bounds to get the range for
     93         * @param zoom The zoom the range should be at
     94         * @return The range for the given bounds.
     95         */
     96        public TileRange toRangeAtZoom(Bounds bounds, int zoom);
     97
     98        /**
     99         * Gets the bounds for a tile
     100         * @param tile The tile
     101         * @return The bounds for that tile
     102         */
     103        public Bounds getBounds(TilePosition tile);
     104
     105        /**
     106         * Checks if an image is available for the given tile
     107         * @param tile The tile to check
     108         * @return True if it is available.
     109         */
     110        public boolean isAvailable(TilePosition tile);
     111
     112    }
     113
     114}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java b/src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java
    new file mode 100644
    index 0000000..c823341
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import java.io.Serializable;
     5
     6import org.openstreetmap.gui.jmapviewer.Tile;
     7import org.openstreetmap.gui.jmapviewer.TileXY;
     8
     9/**
     10 * The position of a single tile. In contrast to {@link TileXY}, this stores the position of the whole tile.
     11 * @author Michael Zangl
     12 * @since xxx
     13 */
     14public class TilePosition implements Serializable {
     15    private static final long serialVersionUID = 1;
     16
     17    private final int x;
     18    private final int y;
     19    private final int zoom;
     20
     21    /**
     22     * Create a new tile position object
     23     * @param x The x coordinate
     24     * @param y The y coordinate
     25     * @param zoom The zoom at which the tile is.
     26     */
     27    TilePosition(int x, int y, int zoom) {
     28        super();
     29        this.x = x;
     30        this.y = y;
     31        this.zoom = zoom;
     32    }
     33
     34    /**
     35     * Create a new tile position object
     36     * @param tile The tile from wich the position should be copied.
     37     */
     38    public TilePosition(Tile tile) {
     39        this(tile.getXtile(), tile.getYtile(), tile.getZoom());
     40    }
     41
     42    /**
     43     * @return the x position
     44     */
     45    public int getX() {
     46        return x;
     47    }
     48
     49    /**
     50     * @return the y position
     51     */
     52    public int getY() {
     53        return y;
     54    }
     55
     56    /**
     57     * @return the zoom
     58     */
     59    public int getZoom() {
     60        return zoom;
     61    }
     62
     63    /**
     64     * Gets an x/y coordinate inside this tile
     65     * @param du x delta. Range should be 0..1
     66     * @param dv y delta. Range should be 0..1
     67     * @return The x/y coordinate
     68     */
     69    public TileXY uv(double du, double dv) {
     70        return new TileXY(getX() + du, getY() + dv);
     71    }
     72
     73    /* (non-Javadoc)
     74     * @see java.lang.Object#hashCode()
     75     */
     76    @Override
     77    public int hashCode() {
     78        final int prime = 31;
     79        int result = 1;
     80        result = prime * result + x;
     81        result = prime * result + y;
     82        result = prime * result + zoom;
     83        return result;
     84    }
     85
     86    /* (non-Javadoc)
     87     * @see java.lang.Object#equals(java.lang.Object)
     88     */
     89    @Override
     90    public boolean equals(Object obj) {
     91        if (this == obj)
     92            return true;
     93        if (obj == null)
     94            return false;
     95        if (getClass() != obj.getClass())
     96            return false;
     97        TilePosition other = (TilePosition) obj;
     98        if (x != other.x)
     99            return false;
     100        if (y != other.y)
     101            return false;
     102        if (zoom != other.zoom)
     103            return false;
     104        return true;
     105    }
     106
     107    @Override
     108    public String toString() {
     109        return "TilePosition [x=" + x + ", y=" + y + ", zoom=" + zoom + "]";
     110    }
     111}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java
    new file mode 100644
    index 0000000..901447d
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import java.util.Comparator;
     5import java.util.function.Function;
     6import java.util.stream.IntStream;
     7import java.util.stream.Stream;
     8
     9import org.openstreetmap.gui.jmapviewer.TileXY;
     10
     11/**
     12 * This is a rectangular range of tiles.
     13 */
     14class TileRange {
     15    int minX;
     16    int maxX;
     17    int minY;
     18    int maxY;
     19    int zoom;
     20
     21    TileRange() {
     22    }
     23
     24    protected TileRange(TileXY t1, TileXY t2, int zoom) {
     25        minX = (int) Math.floor(Math.min(t1.getX(), t2.getX()));
     26        minY = (int) Math.floor(Math.min(t1.getY(), t2.getY()));
     27        maxX = (int) Math.ceil(Math.max(t1.getX(), t2.getX()));
     28        maxY = (int) Math.ceil(Math.max(t1.getY(), t2.getY()));
     29        this.zoom = zoom;
     30    }
     31
     32    protected double tilesSpanned() {
     33        return Math.sqrt(1.0 * this.size());
     34    }
     35
     36    protected int size() {
     37        int xSpan = maxX - minX + 1;
     38        int ySpan = maxY - minY + 1;
     39        return xSpan * ySpan;
     40    }
     41
     42    /**
     43     * @return comparator, that sorts the tiles from the center to the edge of the current screen
     44     */
     45    private Comparator<TilePosition> getTileDistanceComparator() {
     46        final int centerX = (int) Math.ceil((minX + maxX) / 2d);
     47        final int centerY = (int) Math.ceil((minY + maxY) / 2d);
     48        return Comparator.comparingInt(t -> Math.abs(t.getX() - centerX) + Math.abs(t.getY() - centerY));
     49    }
     50
     51    /**
     52     * Gets a stream of all tile positions in this set
     53     * @return A stream of all positions
     54     */
     55    public Stream<TilePosition> tilePositions() {
     56        if (zoom == 0) {
     57            return Stream.empty();
     58        } else {
     59            return IntStream.rangeClosed(minX, maxX).mapToObj(
     60                    x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
     61                    ).flatMap(Function.identity());
     62        }
     63    }
     64
     65    /**
     66     * Gets all tile positions with the ones in the center of the view first.
     67     * @return The tile positions
     68     * @see #tilePositions()
     69     */
     70    public Stream<TilePosition> tilePositionsSorted() {
     71        return tilePositions().sorted(getTileDistanceComparator());
     72    }
     73
     74    /**
     75     * Get the zoom level this range is for
     76     * @return The zoom.
     77     */
     78    public int getZoom() {
     79        return zoom;
     80    }
     81}
  • src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java
    index 67ab88a..f766ac2 100644
    a b public class TileSourceDisplaySettings {  
    212212     * @param changedSetting The setting name
    213213     */
    214214    private void fireSettingsChange(String changedSetting) {
    215         DisplaySettingsChangeEvent e = new DisplaySettingsChangeEvent(changedSetting);
     215        DisplaySettingsChangeEvent e = new DisplaySettingsChangeEvent(this, changedSetting);
    216216        for (DisplaySettingsChangeListener l : listeners) {
    217217            l.displaySettingsChanged(e);
    218218        }
    public class TileSourceDisplaySettings {  
    332332     * @author Michael Zangl
    333333     */
    334334    public static final class DisplaySettingsChangeEvent {
     335        private final TileSourceDisplaySettings source;
    335336        private final String changedSetting;
    336337
    337         DisplaySettingsChangeEvent(String changedSetting) {
     338        DisplaySettingsChangeEvent(TileSourceDisplaySettings source, String changedSetting) {
     339            this.source = source;
    338340            this.changedSetting = changedSetting;
    339341        }
    340342
    341343        /**
     344         * Gets the display settings that caused this event.
     345         * @return The settings.
     346         * @since xxx
     347         */
     348        public TileSourceDisplaySettings getSource() {
     349            return source;
     350        }
     351
     352        /**
    342353         * Gets the setting that was changed
    343354         * @return The name of the changed setting.
    344355         */
  • new file src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java
    new file mode 100644
    index 0000000..9dd7ad7
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Color;
     7import java.awt.Font;
     8import java.awt.Graphics2D;
     9import java.awt.GridBagLayout;
     10import java.awt.Image;
     11import java.awt.Rectangle;
     12import java.awt.Shape;
     13import java.awt.Toolkit;
     14import java.awt.event.ActionEvent;
     15import java.awt.event.MouseAdapter;
     16import java.awt.event.MouseEvent;
     17import java.awt.geom.AffineTransform;
     18import java.awt.geom.Rectangle2D;
     19import java.awt.image.BufferedImage;
     20import java.awt.image.ImageObserver;
     21import java.io.IOException;
     22import java.text.MessageFormat;
     23import java.text.SimpleDateFormat;
     24import java.util.ArrayList;
     25import java.util.Arrays;
     26import java.util.Collections;
     27import java.util.Date;
     28import java.util.List;
     29import java.util.Map.Entry;
     30import java.util.Objects;
     31import java.util.stream.Stream;
     32
     33import javax.swing.AbstractAction;
     34import javax.swing.Action;
     35import javax.swing.BorderFactory;
     36import javax.swing.JLabel;
     37import javax.swing.JMenuItem;
     38import javax.swing.JOptionPane;
     39import javax.swing.JPanel;
     40import javax.swing.JPopupMenu;
     41import javax.swing.JSeparator;
     42import javax.swing.JTextField;
     43import javax.swing.event.PopupMenuEvent;
     44import javax.swing.event.PopupMenuListener;
     45
     46import org.openstreetmap.gui.jmapviewer.Tile;
     47import org.openstreetmap.gui.jmapviewer.TileXY;
     48import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     49import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
     50import org.openstreetmap.josm.Main;
     51import org.openstreetmap.josm.data.Bounds;
     52import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     53import org.openstreetmap.josm.data.projection.Projection;
     54import org.openstreetmap.josm.gui.ExtendedDialog;
     55import org.openstreetmap.josm.gui.MapView;
     56import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle;
     57import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     58import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
     59import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
     60import org.openstreetmap.josm.gui.layer.ImageryLayer;
     61import org.openstreetmap.josm.gui.layer.MapViewGraphics;
     62import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter;
     63import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent;
     64import org.openstreetmap.josm.tools.GBC;
     65import org.openstreetmap.josm.tools.Pair;
     66
     67/**
     68 * This class is used to paint a {@link AbstractTileSourceLayer} to a given map view.
     69 * @author Michael Zangl
     70 * @param <T> The imagery type to use
     71 * @since xxx
     72 */
     73public class TileSourcePainter<T extends AbstractTMSTileSource> extends AbstractTileSourceLoader<T> implements LayerPainter {
     74    /**
     75     *
     76     */
     77    protected final AbstractTileSourceLayer<T> layer;
     78    private static final Font INFO_FONT = new Font("sansserif", Font.BOLD, 13);
     79    /**
     80     * Absolute maximum of tiles to paint
     81     */
     82    private static final int MAX_TILES = 500;
     83
     84    protected final ZoomLevelManager zoom;
     85
     86    private final TextPainter textPainter;
     87
     88    private TilePosition highlightPosition;
     89
     90    final MouseAdapter adapter = new TilePainterMouseAdapter();
     91
     92    private final MapView mapView;
     93
     94    /**
     95     * Create a new {@link TileSourcePainter}
     96     * @param layer The layer to paint
     97     * @param mapView The map view to paint for.
     98     */
     99    public TileSourcePainter(AbstractTileSourceLayer<T> layer, MapView mapView) {
     100        super(layer);
     101        this.layer = layer;
     102        this.mapView = mapView;
     103        mapView.addMouseListener(adapter);
     104
     105        textPainter = new TextPainter();
     106        zoom = new ZoomLevelManager(getSettings(), tileSource, generateCoordinateConverter());
     107        zoom.setZoomBounds(layer.getInfo());
     108    }
     109
     110    @Override
     111    public void paint(MapViewGraphics graphics) {
     112        boolean hasAllocated = allocateCacheMemory();
     113
     114        textPainter.start(graphics.getDefaultGraphics());
     115
     116        if (hasAllocated) {
     117            doPaint(graphics);
     118        } else {
     119            textPainter.addTextOverlay(tr("There is noth enough memory to display this layer."));
     120        }
     121    }
     122
     123    private void doPaint(MapViewGraphics graphics) {
     124        MapViewRectangle pb = graphics.getClipBounds();
     125
     126        drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb);
     127    }
     128
     129    private void drawInViewArea(Graphics2D g, MapView mapView, MapViewRectangle rect) {
     130        g.setFont(INFO_FONT);
     131        TileCoordinateConverter converter = generateCoordinateConverter();
     132        zoom.updateZoomLevel(converter, this);
     133        loadTilesInView(converter);
     134
     135        TileRange baseRange = converter.getViewAtZoom(zoom.getCurrentZoomLevel());
     136
     137        Shape clip = g.getClip();
     138        g.setClip(converter.getProjectionClip());
     139        Stream<TilePosition> area;
     140        if (getSettings().isAutoZoom()) {
     141            area = TileForAreaFinder.getWithFallbackZoom(baseRange, this, zoom);
     142        } else {
     143            area = TileForAreaFinder.getAtDefaultZoom(baseRange, this);
     144        }
     145        paintTileImages(g, area);
     146        g.setClip(clip);
     147
     148        if (highlightPosition != null) {
     149            paintHighlight(g, converter, highlightPosition);
     150        }
     151        paintStatus(baseRange, mapView.getProjection());
     152        paintAttribution(g, rect);
     153        if (Main.isDebugEnabled()) {
     154            paintDebug();
     155        }
     156    }
     157
     158    /**
     159     * Paints a highlight rectangle around a tile.
     160     * @param g
     161     * @param converter
     162     * @param tile
     163     */
     164    private static void paintHighlight(Graphics2D g, TileCoordinateConverter converter, TilePosition tile) {
     165        MapViewLatLonRectangle area = converter.getAreaForTile(tile);
     166        g.setColor(Color.RED);
     167        g.draw(area.getInView());
     168    }
     169
     170    /**
     171     * Paint the filtered images for the given tiles
     172     * @param g The graphics to paint on
     173     * @param area The tiles to paint.
     174     */
     175    private void paintTileImages(Graphics2D g, Stream<TilePosition> area) {
     176        TileCoordinateConverter converter = generateCoordinateConverter();
     177        Rectangle b = g.getClipBounds();
     178        int maxTiles = (int) (b.getWidth() * b.getHeight() / tileSource.getTileSize() / tileSource.getTileSize() * 5);
     179        List<Tile> errorTiles = Collections.synchronizedList(new ArrayList<>());
     180        Stream<Tile> tiles = area.parallel()
     181            .limit(Math.min(maxTiles, MAX_TILES))
     182            .map(this::getTile)
     183            .filter(Objects::nonNull);
     184
     185        if (getSettings().isShowErrors()) {
     186            tiles = tiles.peek(t -> { if (t.hasError()) errorTiles.add(t); });
     187        }
     188        tiles.map(tile -> new Pair<>(tile, tile.getImage()))
     189            .filter(p -> imageLoaded(p.b))
     190            .map(p -> new Pair<>(p.a, layer.applyImageProcessors(p.b)))
     191            .forEachOrdered(p -> paintTileImage(g, p.a, p.b, converter));
     192
     193        for (Tile error : errorTiles) {
     194            textPainter.drawTileString(tr("Error") + ": " + tr(error.getErrorMessage()),
     195                    new TilePosition(error), converter);
     196        }
     197    }
     198
     199    /**
     200     * We only paint full tile images.
     201     * <p>
     202     * We handle that the correct tile images are in front by sorting the list of tiles accordingly.
     203     * @param g The graphics to paint on
     204     * @param tile The tile to paint
     205     * @param image The image to paint for the tile
     206     * @param converter The coordinate converter.
     207     */
     208    private void paintTileImage(Graphics2D g, Tile tile, BufferedImage image, TileCoordinateConverter converter) {
     209        AffineTransform transform = converter.getTransformForTile(new TilePosition(tile), 0, 0, 0, 1, 1, 1);
     210        transform.scale(1.0 / image.getWidth(), 1.0 / image.getHeight());
     211
     212        g.drawImage(image, transform, layer);
     213
     214        if (ImageryLayer.PROP_FADE_AMOUNT.get() != 0) {
     215            // dimm by painting opaque rect...
     216            // TODO: Convert this to a filter.
     217            g.setColor(ImageryLayer.getFadeColorWithAlpha());
     218            AffineTransform oldTrans = g.getTransform();
     219            g.transform(transform);
     220            g.fillRect(0, 0, image.getWidth(), image.getHeight());
     221            g.setTransform(oldTrans);
     222        }
     223
     224        if (Main.isTraceEnabled()) {
     225            textPainter.drawTileString(tile.getKey(), new TilePosition(tile), converter);
     226        }
     227    }
     228
     229    private void paintAttribution(Graphics2D defaultGraphics, MapViewRectangle rect) {
     230        Rectangle2D inView = rect.getInView();
     231        Graphics2D g = (Graphics2D) defaultGraphics.create();
     232        g.translate(inView.getMinX(), inView.getMinY());
     233        Bounds boundsBox = rect.getLatLonBoundsBox();
     234        attribution.paintAttribution(g, (int) inView.getWidth(), (int) inView.getHeight(),
     235                boundsBox.getMin().toCoordinate(), boundsBox.getMax().toCoordinate(), zoom.getDisplayZoomLevel(),
     236                layer);
     237    }
     238
     239    private void paintStatus(TileRange baseRange, Projection projection) {
     240        if (isTooLarge(baseRange)) {
     241            textPainter.addTextOverlay(tr("zoom in to load more tiles"));
     242        } else if (!getSettings().isAutoZoom() && isTooSmall(baseRange)) {
     243            textPainter.addTextOverlay(tr("increase tiles zoom level (change resolution) to see more detail"));
     244        } else if (getSettings().isAutoZoom() && getSettings().isAutoLoad() && !hasTiles(baseRange, TileSourcePainter::isVisible)
     245                && (!hasTiles(baseRange, TileSourcePainter::isLoading) || hasTiles(baseRange, TileSourcePainter::isOverzoomed))) {
     246            textPainter.addTextOverlay(tr("No tiles at this zoom level"));
     247        }
     248
     249        if (!isProjectionSupported(projection)) {
     250            textPainter.addTextOverlay(tr("The tile source does not support the current projection natively"));
     251        }
     252    }
     253
     254    private void paintDebug() {
     255        for (String s : zoom.getDebugInfo(generateCoordinateConverter())) {
     256            textPainter.addDebug(s);
     257        }
     258        textPainter.addDebug(tr("Estimated cache size: {0}", estimateTileCacheSize()));
     259        if (tileLoader instanceof TMSCachedTileLoader) {
     260            TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
     261            for (String part : cachedTileLoader.getStats().split("\n")) {
     262                textPainter.addDebug(tr("Cache stats: {0}", part));
     263            }
     264        }
     265    }
     266
     267    private void loadTilesInView(TileCoordinateConverter converter) {
     268        int zoomToLoad = zoom.getDisplayZoomLevel();
     269        TileRange range = converter.getViewAtZoom(zoomToLoad);
     270
     271        if (getSettings().isAutoZoom()) {
     272        // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
     273        // to make sure there're really no more zoom levels
     274        if (zoomToLoad < zoom.getCurrentZoomLevel() && !hasTiles(range, TileSourcePainter::isMissing)) {
     275            zoomToLoad++;
     276            range = converter.getViewAtZoom(zoomToLoad);
     277        } else  {
     278            // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
     279            // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
     280            // loading is done in the next if section
     281            while (zoomToLoad > zoom.getMinZoom() && hasTiles(range, TileSourcePainter::isOverzoomed)
     282                    && !hasTiles(range, TileSourcePainter::isMissing)) {
     283                zoomToLoad--;
     284                range = converter.getViewAtZoom(zoomToLoad);
     285            }
     286        }
     287        }
     288        loadTiles(range, false);
     289    }
     290
     291    private void loadErrorTiles(TileRange range, boolean force) {
     292        if (getSettings().isAutoLoad() || force) {
     293            range.tilePositionsSorted().map(this::getOrCreateTile).filter(Tile::hasError)
     294                    .forEach(t -> tileLoader.createTileLoaderJob(t).submit(force));
     295        }
     296    }
     297
     298    protected void loadAllErrorTiles(boolean force) {
     299        loadErrorTiles(generateCoordinateConverter().getViewAtZoom(zoom.getCurrentZoomLevel()), force);
     300    }
     301
     302    protected void loadAllTiles(boolean force) {
     303        loadTiles(generateCoordinateConverter().getViewAtZoom(zoom.getCurrentZoomLevel()), force);
     304    }
     305
     306    @Override
     307    protected void loadTiles(TileRange range, boolean force) {
     308        super.loadTiles(range, force || getSettings().isAutoLoad());
     309    }
     310
     311    private boolean imageLoaded(Image i) {
     312        if (i == null) {
     313            return false;
     314        } else {
     315            int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, layer);
     316            return (status & ImageObserver.ALLBITS) != 0;
     317        }
     318    }
     319
     320    private TileCoordinateConverter generateCoordinateConverter() {
     321        return new TileCoordinateConverter(mapView.getState(), tileSource, getSettings());
     322    }
     323
     324    /**
     325     * Check whether this layer supports the given projection
     326     * @param projection The projection to search
     327     * @return <code>true</code> if supported.
     328     */
     329    protected boolean isProjectionSupported(Projection projection) {
     330        return true;
     331    }
     332
     333    private TileSourceDisplaySettings getSettings() {
     334        return layer.getDisplaySettings();
     335    }
     336
     337    /**
     338     * Gets the menu entries for this layer
     339     * @return The menu entries
     340     */
     341    public List<Action> getMenuEntries() {
     342        return Arrays.asList(zoom.new IncreaseZoomAction(), zoom.new DecreaseZoomAction(),
     343                zoom.new ZoomToBestAction(mapView), zoom.new ZoomToNativeLevelAction(mapView),
     344                new FlushTileCacheAction(), new LoadErroneusTilesAction(), new LoadAllTilesAction());
     345    }
     346
     347    /**
     348     * Gets the current zoom level as String
     349     * @return The zoom level.
     350     */
     351    public String getZoomString() {
     352        return Integer.toString(zoom.getCurrentZoomLevel());
     353    }
     354
     355    @Override
     356    public void detachFromMapView(MapViewEvent event) {
     357        event.getMapView().removeMouseListener(adapter);
     358        MapView.removeZoomChangeListener(this);
     359        freeCacheMemory();
     360        layer.detach(this);
     361    }
     362
     363    private final class TilePainterMouseAdapter extends MouseAdapter {
     364        @Override
     365        public void mouseClicked(MouseEvent e) {
     366            if (e.getButton() == MouseEvent.BUTTON3) {
     367                TilePosition tilePos = getTileForPixelpos(mapView.getState().getForView(e.getPoint()));
     368                JPopupMenu popup = layer.new TileSourceLayerPopup(mapView);
     369                popup.addPopupMenuListener(new PopupMenuListener() {
     370                    @Override
     371                    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
     372                        highlightPosition = tilePos;
     373                        // triggers repaint
     374                        layer.invalidate();
     375                    }
     376
     377                    @Override
     378                    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
     379                        highlightPosition = null;
     380                        layer.invalidate();
     381                    }
     382
     383                    @Override
     384                    public void popupMenuCanceled(PopupMenuEvent e) {
     385                        // ignore
     386                    }
     387                });
     388                if (tilePos != null) {
     389                    popup.add(new JSeparator());
     390                    popup.add(new JMenuItem(new LoadTileAction(tilePos)));
     391                    Tile tile = getOrCreateTile(tilePos);
     392                    if (tile != null) {
     393                        popup.add(new JMenuItem(new ShowTileInfoAction(tile)));
     394                    }
     395                }
     396                popup.show(e.getComponent(), e.getX(), e.getY());
     397            } else if (e.getButton() == MouseEvent.BUTTON1) {
     398                attribution.handleAttribution(e.getPoint(), true);
     399            }
     400        }
     401
     402        /**
     403         * Returns tile for a pixel position.<p>
     404         * This isn't very efficient, but it is only used when the user right-clicks on the map.
     405         * @param mapViewPoint pixel coordinate
     406         * @return Tile at pixel position
     407         */
     408        private TilePosition getTileForPixelpos(MapViewPoint mapViewPoint) {
     409            Main.trace("getTileForPixelpos({0})", mapViewPoint);
     410            TileCoordinateConverter converter = generateCoordinateConverter();
     411
     412            TileRange ts = converter.getViewAtZoom(zoom.getCurrentZoomLevel());
     413
     414            Stream<TilePosition> clickedTiles = ts.tilePositions()
     415                    .filter(t -> converter.getAreaForTile(t).contains(mapViewPoint));
     416            if (Main.isTraceEnabled()) {
     417                clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: {0}, {1};  currentZoomLevel: {2}",
     418                        t.getX(), t.getY(), zoom.getCurrentZoomLevel()));
     419            }
     420            return clickedTiles.findAny().orElse(null);
     421        }
     422    }
     423
     424    private class LoadAllTilesAction extends AbstractAction {
     425        LoadAllTilesAction() {
     426            super(tr("Load all tiles"));
     427        }
     428
     429        @Override
     430        public void actionPerformed(ActionEvent ae) {
     431            loadAllTiles(true);
     432        }
     433    }
     434
     435    private class LoadErroneusTilesAction extends AbstractAction {
     436        LoadErroneusTilesAction() {
     437            super(tr("Load all error tiles"));
     438        }
     439
     440        @Override
     441        public void actionPerformed(ActionEvent ae) {
     442            loadAllErrorTiles(true);
     443        }
     444    }
     445
     446    private final class ShowTileInfoAction extends AbstractAction {
     447
     448        private final transient Tile clickedTile;
     449
     450        private ShowTileInfoAction(Tile clickedTile) {
     451            super(tr("Show tile info"));
     452            this.clickedTile = clickedTile;
     453        }
     454
     455        private String getSizeString(int size) {
     456            StringBuilder ret = new StringBuilder();
     457            return ret.append(size).append('x').append(size).toString();
     458        }
     459
     460        private JTextField createTextField(String text) {
     461            JTextField ret = new JTextField(text);
     462            ret.setEditable(false);
     463            ret.setBorder(BorderFactory.createEmptyBorder());
     464            return ret;
     465        }
     466
     467        @Override
     468        public void actionPerformed(ActionEvent ae) {
     469            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[] { tr("OK") });
     470            JPanel panel = new JPanel(new GridBagLayout());
     471            MapViewLatLonRectangle displaySize = generateCoordinateConverter().getAreaForTile(new TilePosition(clickedTile));
     472            Rectangle2D bounds = displaySize.getInView().getBounds2D();
     473            String[][] content = { { "Tile name", clickedTile.getKey() }, { "Tile url", getUrl() },
     474                    { "Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
     475                    { "Position in view", MessageFormat.format("x={0}..{1}, y={2}..{3}",
     476                            bounds.getMinX(), bounds.getMaxX(),
     477                            bounds.getMinY(), bounds.getMaxY())},
     478                    { "Position on projection",MessageFormat.format("east={0}..{1}, north={2}..{3}",
     479                            displaySize.getProjectionBounds().minEast, displaySize.getProjectionBounds().maxEast,
     480                            displaySize.getProjectionBounds().minNorth, displaySize.getProjectionBounds().maxNorth)},
     481                    { "Position on world",MessageFormat.format("lat={0}..{1}, lon={2}..{3}",
     482                            displaySize.getLatLonBoundsBox().getMinLat(), displaySize.getLatLonBoundsBox().getMaxLat(),
     483                            displaySize.getLatLonBoundsBox().getMinLon(), displaySize.getLatLonBoundsBox().getMaxLon())},
     484            };
     485
     486            for (String[] entry : content) {
     487                panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
     488                panel.add(GBC.glue(5, 0), GBC.std());
     489                panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
     490            }
     491
     492            for (Entry<String, String> e : clickedTile.getMetadata().entrySet()) {
     493                panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());
     494                panel.add(GBC.glue(5, 0), GBC.std());
     495                String value = e.getValue();
     496                if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
     497                    value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
     498                }
     499                panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
     500
     501            }
     502            ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
     503            ed.setContent(panel);
     504            ed.showDialog();
     505        }
     506
     507        private String getUrl() {
     508            try {
     509                return clickedTile.getUrl();
     510            } catch (IOException e) {
     511                // silence exceptions
     512                Main.trace(e);
     513                return "";
     514            }
     515        }
     516    }
     517
     518    private final class LoadTileAction extends AbstractAction {
     519
     520        private final transient TilePosition clickedTile;
     521
     522        private LoadTileAction(TilePosition clickedTile) {
     523            super(tr("Load tile"));
     524            this.clickedTile = clickedTile;
     525            setEnabled(clickedTile != null);
     526        }
     527
     528        @Override
     529        public void actionPerformed(ActionEvent ae) {
     530            loadTile(clickedTile, true);
     531            layer.invalidate();
     532        }
     533    }
     534
     535    @Override
     536    public Bounds getBounds(TilePosition tilePos) {
     537        ICoordinate min = tileSource.tileXYToLatLon(tilePos.getX(), tilePos.getY(), tilePos.getZoom());
     538        ICoordinate max = tileSource.tileXYToLatLon(tilePos.getX() + 1, tilePos.getY() + 1, tilePos.getZoom());
     539        Bounds bounds = new Bounds(min.getLat(), min.getLon(), false);
     540        bounds.extend(max.getLat(), max.getLon());
     541        return bounds;
     542    }
     543
     544    @Override
     545    public TileRange toRangeAtZoom(Bounds bounds, int zoom) {
     546        TileXY t1 = tileSource.latLonToTileXY(bounds.getMinLat(), bounds.getMinLon(), zoom);
     547        TileXY t2 = tileSource.latLonToTileXY(bounds.getMaxLat(), bounds.getMaxLon(), zoom);
     548        return new TileRange(t1, t2, zoom);
     549    }
     550
     551    @Override
     552    public boolean isAvailable(TilePosition tilePos) {
     553        Tile tile = getTile(tilePos);
     554        return tile != null && !tile.hasError()
     555                && !(isOverzoomed(tile) && tilePos.getZoom() > zoom.getDisplayZoomLevel())
     556                && isImageAvailable(tile);
     557    }
     558
     559    private boolean isImageAvailable(Tile tile) {
     560        BufferedImage image = tile.getImage();
     561        return imageLoaded(image);
     562    }
     563}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java b/src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java
    new file mode 100644
    index 0000000..fe0fea4
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.event.ActionEvent;
     7import java.text.MessageFormat;
     8import java.util.Arrays;
     9import java.util.List;
     10
     11import javax.swing.AbstractAction;
     12
     13import org.openstreetmap.gui.jmapviewer.Tile;
     14import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     15import org.openstreetmap.josm.Main;
     16import org.openstreetmap.josm.data.imagery.ImageryInfo;
     17import org.openstreetmap.josm.gui.MapView;
     18import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
     19
     20/**
     21 * This class manages the zoom level of a {@link TileSourcePainter}
     22 * @author Michael Zangl
     23 * @since xxx
     24 */
     25public class ZoomLevelManager {
     26    /**
     27     * Zoomlevel selected by the user.
     28     */
     29    private int currentZoomLevel;
     30    /**
     31     * The zoom level at which tiles are currently displayed
     32     */
     33    private int displayZoomLevel;
     34    private TileSourceDisplaySettings settings;
     35    private TileSource source;
     36
     37    private int minZoom;
     38    private int maxZoom;
     39
     40    /**
     41     * Create a new zoom level manager
     42     * @param settings The zoom settings
     43     * @param source The tile source to use.
     44     * @param initialZoomState The initial state to compute the zoom factor from.
     45     */
     46    public ZoomLevelManager(TileSourceDisplaySettings settings, TileSource source, TileCoordinateConverter initialZoomState) {
     47        this.settings = settings;
     48        this.source = source;
     49
     50        setZoomLevel(clampZoom(initialZoomState.getBestZoom()));
     51        setZoomBounds(source.getMinZoom(), source.getMaxZoom());
     52    }
     53
     54    /**
     55     * Set the zoom bounds
     56     * @param bounds An info to get the zoom bounds from
     57     */
     58    public void setZoomBounds(ImageryInfo bounds) {
     59        setZoomBounds(bounds.getMinZoom(), bounds.getMaxZoom());
     60    }
     61
     62    /**
     63     * Sets the zoom bounds
     64     * @param minZoom The minimum zoom
     65     * @param maxZoom The maximum zoom.
     66     */
     67    public void setZoomBounds(int minZoom, int maxZoom) {
     68        if (minZoom > maxZoom || minZoom < 0) {
     69            throw new IllegalArgumentException(MessageFormat.format("Zoom range not valid: {0}..{1}", minZoom, maxZoom));
     70        }
     71        this.minZoom = AbstractTileSourceLayer.checkMinZoomLvl(minZoom, source);
     72        this.maxZoom = AbstractTileSourceLayer.checkMaxZoomLvl(maxZoom, source);
     73    }
     74
     75    /**
     76     * @return The min zoom that was set.
     77     */
     78    public int getMinZoom() {
     79        return minZoom;
     80    }
     81
     82    /**
     83     * @return The max zoom that was set.
     84     */
     85    public int getMaxZoom() {
     86        return maxZoom;
     87    }
     88
     89    /**
     90     *
     91     * @return if its allowed to zoom in
     92     */
     93    public boolean zoomIncreaseAllowed() {
     94        boolean zia = currentZoomLevel < this.getMaxZoom();
     95        if (Main.isDebugEnabled()) {
     96            Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoom());
     97        }
     98        return zia;
     99    }
     100
     101    /**
     102     * Zoom in, go closer to map.
     103     *
     104     * @return    true, if zoom increasing was successful, false otherwise
     105     */
     106    public boolean increaseZoomLevel() {
     107        return setZoomLevel(currentZoomLevel + 1);
     108    }
     109
     110    /**
     111     * Sets the zoom level of the layer
     112     * @param zoom zoom level
     113     * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
     114     */
     115    public boolean setZoomLevel(int zoom) {
     116        if (zoom == currentZoomLevel && zoom == displayZoomLevel) {
     117            return true;
     118        } else if (zoom < getMinZoom() || zoom > getMaxZoom()) {
     119            Main.warn("Current zoom level ({0}) could not be changed to {1}: out of range {2} .. {3}", currentZoomLevel,
     120                    zoom, getMinZoom(), getMaxZoom());
     121            return false;
     122        } else {
     123            Main.debug("changing zoom level to: {0}", currentZoomLevel);
     124            currentZoomLevel = zoom;
     125            displayZoomLevel = zoom;
     126            return true;
     127        }
     128    }
     129
     130    /**
     131     * Check if zooming out is allowed
     132     *
     133     * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
     134     */
     135    public boolean zoomDecreaseAllowed() {
     136        boolean zda = currentZoomLevel > this.getMinZoom();
     137        if (Main.isDebugEnabled()) {
     138            Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoom());
     139        }
     140        return zda;
     141    }
     142
     143    /**
     144     * Zoom out from map.
     145     *
     146     * @return    true, if zoom increasing was successfull, false othervise
     147     */
     148    public boolean decreaseZoomLevel() {
     149        return setZoomLevel(currentZoomLevel - 1);
     150    }
     151
     152    /**
     153     * Update the zoom level that should be displayed
     154     * @param currentZoomState The coordinate converter that holds the current view
     155     * @param viewStatus An accessor for finding out if the given tiles are available.
     156     */
     157    public void updateZoomLevel(TileCoordinateConverter currentZoomState, TileSourcePainter<?> viewStatus) {
     158        if (settings.isAutoZoom()) {
     159            int zoom = clampZoom(currentZoomState.getBestZoom());
     160            setZoomLevel(zoom);
     161            if (settings.isAutoLoad()) {
     162                // Find highest zoom level with at least one visible tile
     163
     164                for (int tmpZoom = zoom; tmpZoom >= getMinZoom(); tmpZoom--) {
     165                    TileRange area = currentZoomState.getViewAtZoom(zoom);
     166                    if (viewStatus.hasTiles(area, ZoomLevelManager::visibleOrOverzoomed)) {
     167                        displayZoomLevel = tmpZoom;
     168                        break;
     169                    }
     170                }
     171            }
     172        } else {
     173            displayZoomLevel = currentZoomLevel;
     174        }
     175    }
     176
     177    private static boolean visibleOrOverzoomed(Tile t) {
     178        return TileSourcePainter.isVisible(t) || TileSourcePainter.isOverzoomed(t);
     179    }
     180
     181    private int clampZoom(int intResult) {
     182        return Math.max(Math.min(intResult, getMaxZoom()), getMinZoom());
     183    }
     184
     185    /**
     186     * Gets the current zoom level that is requested by the user for displaying the tiles.
     187     * @return The current zoom level of the view
     188     */
     189    public int getCurrentZoomLevel() {
     190        return currentZoomLevel;
     191    }
     192
     193    /**
     194     * Gets the zoom level that is suggested to be displayed. This may be different depending on the tile loading settings.
     195     * @return The suggested zoom.
     196     */
     197    public int getDisplayZoomLevel() {
     198        return displayZoomLevel;
     199    }
     200
     201    /**
     202     * Gets the debug information that should be added about the zoom level.
     203     * @param currentZoomState A coordinate converter
     204     * @return The current zoom status.
     205     */
     206    public List<String> getDebugInfo(TileCoordinateConverter currentZoomState) {
     207        int bestZoom = currentZoomState.getBestZoom();
     208        return Arrays.asList(
     209                tr("Current zoom: {0}", getCurrentZoomLevel()),
     210                tr("Display zoom: {0}", displayZoomLevel),
     211                tr("Pixel scale: {0}", currentZoomState.getScaleFactor(getCurrentZoomLevel())),
     212                tr("Best zoom: {0} (clamped to: {1})", bestZoom, clampZoom(bestZoom))
     213                );
     214    }
     215
     216    /**
     217     * Zooms to the native level of the current view
     218     */
     219    public class ZoomToNativeLevelAction extends AbstractAction {
     220        private final MapView forView;
     221
     222        /**
     223         * Create a new {@link ZoomToNativeLevelAction}
     224         * @param forView The map view to zoom
     225         */
     226        public ZoomToNativeLevelAction(MapView forView) {
     227            super(tr("Zoom to native resolution"));
     228            this.forView = forView;
     229        }
     230
     231        @Override
     232        public void actionPerformed(ActionEvent ae) {
     233            TileCoordinateConverter converter = new TileCoordinateConverter(forView.getState(), source, settings);
     234            double newFactor = Math.sqrt(converter.getScaleFactor(currentZoomLevel));
     235            forView.zoomToFactor(newFactor);
     236        }
     237    }
     238
     239    /**
     240     * Zooms the layer to the best display zoom for the current map view state
     241     */
     242    public class ZoomToBestAction extends AbstractAction {
     243        private final int bestZoom;
     244
     245        /**
     246         * Create a new {@link ZoomToBestAction}
     247         * @param forView The view to use as reference.
     248         */
     249        public ZoomToBestAction(MapView forView) {
     250            super(tr("Change resolution"));
     251            bestZoom = clampZoom(new TileCoordinateConverter(forView.getState(), source, settings).getBestZoom());
     252            setEnabled(!settings.isAutoZoom() && bestZoom != currentZoomLevel);
     253        }
     254
     255        @Override
     256        public void actionPerformed(ActionEvent ae) {
     257            setZoomLevel(bestZoom);
     258        }
     259    }
     260
     261    /**
     262     * Increase the zoom by 1.
     263     */
     264    public class IncreaseZoomAction extends AbstractAction {
     265        /**
     266         * Create a new {@link IncreaseZoomAction}
     267         */
     268        public IncreaseZoomAction() {
     269            super(tr("Increase zoom"));
     270            setEnabled(!settings.isAutoZoom() && zoomIncreaseAllowed());
     271        }
     272
     273        @Override
     274        public void actionPerformed(ActionEvent ae) {
     275            increaseZoomLevel();
     276        }
     277    }
     278
     279    /**
     280     * Decrease the zoom by 1.
     281     */
     282    public class DecreaseZoomAction extends AbstractAction {
     283        /**
     284         * Create a new {@link DecreaseZoomAction}
     285         */
     286        public DecreaseZoomAction() {
     287            super(tr("Decrease zoom"));
     288            setEnabled(!settings.isAutoZoom() && zoomDecreaseAllowed());
     289        }
     290
     291        @Override
     292        public void actionPerformed(ActionEvent ae) {
     293            decreaseZoomLevel();
     294        }
     295    }
     296
     297}
  • new file test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java

    diff --git a/test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java b/test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java
    new file mode 100644
    index 0000000..b673d61
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import static org.junit.Assert.assertEquals;
     5
     6import java.awt.Image;
     7import java.awt.Point;
     8import java.awt.geom.AffineTransform;
     9import java.awt.geom.Point2D;
     10import java.io.IOException;
     11import java.util.List;
     12import java.util.Map;
     13
     14import org.junit.Before;
     15import org.junit.BeforeClass;
     16import org.junit.Test;
     17import org.openstreetmap.gui.jmapviewer.Coordinate;
     18import org.openstreetmap.gui.jmapviewer.Tile;
     19import org.openstreetmap.gui.jmapviewer.TileXY;
     20import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     21import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     22import org.openstreetmap.josm.JOSMFixture;
     23import org.openstreetmap.josm.Main;
     24import org.openstreetmap.josm.data.coor.EastNorth;
     25import org.openstreetmap.josm.data.coor.LatLon;
     26import org.openstreetmap.josm.gui.MapFrame;
     27import org.openstreetmap.josm.gui.MapViewState;
     28import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle;
     29import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     30import org.openstreetmap.josm.gui.layer.LayerManagerTest;
     31import org.openstreetmap.josm.gui.util.GuiHelper;
     32
     33/**
     34 * Test {@link TileCoordinateConverter}
     35 * @author Michael Zangl
     36 * @since xxx
     37 */
     38public class TileCoordinateConverterTest {
     39    private static final class TransformingConverter extends TileCoordinateConverter {
     40        AffineTransform transform = new AffineTransform();
     41
     42        private TransformingConverter(MapViewState mapView, TileSource tileSource, TileSourceDisplaySettings settings) {
     43            super(mapView, tileSource, settings);
     44        }
     45
     46        @Override
     47        protected MapViewPoint pos(ICoordinate ll) {
     48            Point2D transformed = transform.transform(new Point2D.Double(ll.getLat(), ll.getLon()), null);
     49            return super.pos(new Coordinate(transformed.getX(), transformed.getY()));
     50        }
     51    }
     52
     53    private static final class TestTileSource implements TileSource {
     54        @Override
     55        public boolean requiresAttribution() {
     56            throw new UnsupportedOperationException();
     57        }
     58
     59        @Override
     60        public String getTermsOfUseURL() {
     61            throw new UnsupportedOperationException();
     62        }
     63
     64        @Override
     65        public String getTermsOfUseText() {
     66            throw new UnsupportedOperationException();
     67        }
     68
     69        @Override
     70        public String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) {
     71            throw new UnsupportedOperationException();
     72        }
     73
     74        @Override
     75        public String getAttributionLinkURL() {
     76            throw new UnsupportedOperationException();
     77        }
     78
     79        @Override
     80        public String getAttributionImageURL() {
     81            throw new UnsupportedOperationException();
     82        }
     83
     84        @Override
     85        public Image getAttributionImage() {
     86            throw new UnsupportedOperationException();
     87        }
     88
     89        @Override
     90        public Point latLonToXY(ICoordinate point, int zoom) {
     91            return latLonToXY(point.getLat(), point.getLon(), zoom);
     92        }
     93
     94        @Override
     95        public ICoordinate xyToLatLon(Point point, int zoom) {
     96            return xyToLatLon(point.x, point.y, zoom);
     97        }
     98
     99        @Override
     100        public TileXY latLonToTileXY(ICoordinate point, int zoom) {
     101            return latLonToTileXY(point.getLat(), point.getLon(), zoom);
     102        }
     103
     104        @Override
     105        public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
     106            return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
     107        }
     108
     109        @Override
     110        public ICoordinate tileXYToLatLon(Tile tile) {
     111            return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
     112        }
     113
     114        @Override
     115        public boolean isNoTileAtZoom(Map<String, List<String>> headers, int statusCode, byte[] content) {
     116            throw new UnsupportedOperationException();
     117        }
     118
     119        @Override
     120        public int getTileYMin(int zoom) {
     121            throw new UnsupportedOperationException();
     122        }
     123
     124        @Override
     125        public int getTileYMax(int zoom) {
     126            throw new UnsupportedOperationException();
     127        }
     128
     129        @Override
     130        public int getTileXMin(int zoom) {
     131            throw new UnsupportedOperationException();
     132        }
     133
     134        @Override
     135        public int getTileXMax(int zoom) {
     136            throw new UnsupportedOperationException();
     137        }
     138
     139        @Override
     140        public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
     141            throw new UnsupportedOperationException();
     142        }
     143
     144        @Override
     145        public int getTileSize() {
     146            throw new UnsupportedOperationException();
     147        }
     148
     149        @Override
     150        public String getTileId(int zoom, int tilex, int tiley) {
     151            throw new UnsupportedOperationException();
     152        }
     153
     154        @Override
     155        public String getName() {
     156            throw new UnsupportedOperationException();
     157        }
     158
     159        @Override
     160        public int getMinZoom() {
     161            throw new UnsupportedOperationException();
     162        }
     163
     164        @Override
     165        public Map<String, String> getMetadata(Map<String, List<String>> headers) {
     166            throw new UnsupportedOperationException();
     167        }
     168
     169        @Override
     170        public int getMaxZoom() {
     171            throw new UnsupportedOperationException();
     172        }
     173
     174        @Override
     175        public String getId() {
     176            throw new UnsupportedOperationException();
     177        }
     178
     179        @Override
     180        public double getDistance(double la1, double lo1, double la2, double lo2) {
     181            throw new UnsupportedOperationException();
     182        }
     183
     184        @Override
     185        public int getDefaultTileSize() {
     186            throw new UnsupportedOperationException();
     187        }
     188
     189        @Override
     190        public Point latLonToXY(double lat, double lon, int zoom) {
     191            return new Point((int) lat / 13, (int) lon - 4);
     192        }
     193
     194        @Override
     195        public ICoordinate xyToLatLon(int x, int y, int zoom) {
     196            return new Coordinate(x * 13, y + 4);
     197        }
     198
     199        @Override
     200        public TileXY latLonToTileXY(double lat, double lon, int zoom) {
     201            return new TileXY(lat / 13, lon - 4);
     202        }
     203
     204        @Override
     205        public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
     206            return new Coordinate(x * 13, y + 4);
     207        }
     208    }
     209
     210    private TransformingConverter converter;
     211
     212    /**
     213     * Setup test.
     214     */
     215    @BeforeClass
     216    public static void setUpBeforeClass() {
     217        JOSMFixture.createUnitTestFixture().init(true);
     218        Main.getLayerManager().addLayer(new LayerManagerTest.TestLayer());
     219        GuiHelper.runInEDTAndWait(() -> {});
     220    }
     221
     222    /**
     223     * Sets up a fake tile source.
     224     */
     225    @Before
     226    public void setUp() {
     227        MapFrame map = Main.map;
     228        map.mapView.zoomTo(new EastNorth(0, 0), 1);
     229        converter = new TransformingConverter(map.mapView.getState(), new TestTileSource(), new TileSourceDisplaySettings());
     230    }
     231
     232    /**
     233     * Test {@link TileCoordinateConverter#getAreaForTile(TilePosition)}
     234     */
     235    @Test
     236    public void testGetAreaForTile() {
     237        EastNorth p1 = Main.getProjection().latlon2eastNorth(new LatLon(26, 7));
     238        MapViewLatLonRectangle rect = converter.getAreaForTile(new TilePosition(2, 3, 1));
     239        assertEquals(p1.getX(), rect.getProjectionBounds().minEast, 1e-10);
     240        assertEquals(p1.getY(), rect.getProjectionBounds().minNorth, 1e-10);
     241    }
     242
     243    /**
     244     * Test {@link TileCoordinateConverter#getTransformForTile(TilePosition, double, double, double, double, double, double)}
     245     */
     246    @Test
     247    public void testGetTransformForTile() {
     248        TilePosition tile = new TilePosition(2, 3, 1);
     249        Point2D p1 = Main.map.mapView.getState().getPointFor(new LatLon(26, 7)).getInView();
     250        Point2D p2 = Main.map.mapView.getState().getPointFor(new LatLon(39, 8)).getInView();
     251
     252        for (AffineTransform transform : new AffineTransform[] {
     253                converter.getTransformForTile(tile, 0, 0, 0, 1, 1, 1),
     254                converter.getTransformForTile(tile, 0, 0, 1, 0, 1, 1),
     255                converter.getTransformForTile(tile, 0, 0, 1, 1, 1, 0),
     256                converter.getTransformForTile(tile, 0, 0, 0, 1, 1, 0),
     257        }) {
     258            assertEquals(p1.getX(), transform.getTranslateX(), 1e-10);
     259            assertEquals(p1.getY(), transform.getTranslateY(), 1e-10);
     260
     261            Point2D p1converted = transform.transform(new Point2D.Double(0, 0), null);
     262            Point2D p2converted = transform.transform(new Point2D.Double(1, 1), null);
     263
     264            assertEquals(p1.getX(), p1converted.getX(), 1e-10);
     265            assertEquals(p1.getY(), p1converted.getY(), 1e-10);
     266            assertEquals(p2.getX(), p2converted.getX(), 1e-10);
     267            assertEquals(p2.getY(), p2converted.getY(), 1e-10);
     268        }
     269    }
     270        /**
     271         * Test {@link TileCoordinateConverter#getTransformForTile(TilePosition, double, double, double, double, double, double)}
     272         * ignores unsolveable
     273         */
     274        @Test
     275        public void testGetTransformForTileIgnoresUnsolveable() {
     276            TilePosition tile = new TilePosition(2, 3, 1);
     277        for (AffineTransform transform : new AffineTransform[] {
     278                converter.getTransformForTile(tile, 0, 0, 0, 0, 1, 1),
     279                converter.getTransformForTile(tile, 0, 0, 1, 0, 0.5, 0),
     280                converter.getTransformForTile(tile, 0, 0, 1, 1, 0, 0),
     281                converter.getTransformForTile(tile, 0, 0, 0, 0e-30, 1, 0),
     282        }) {
     283            assertEquals(1, transform.getScaleX(), 1e-10);
     284            assertEquals(1, transform.getScaleY(), 1e-10);
     285            assertEquals(0, transform.getTranslateX(), 1e-10);
     286            assertEquals(0, transform.getTranslateY(), 1e-10);
     287        }
     288    }
     289}