Ticket #11255: wms_jcs_cache.patch

File wms_jcs_cache.patch, 285.8 KB (added by wiktorn, 11 years ago)
  • build.xml

    diff --git a/build.xml b/build.xml
    index 04febc6..936e8dd 100644
    a b Build-Date: ${build.tstamp}  
    198198            <arg value="${mapcss.dir}/MapCSSParser.jj"/>
    199199        </exec>
    200200    </target>
    201     <target name="-jaxb_win" if="isWindows">
    202         <property name="xjc" value="${java.home}\..\bin\xjc.exe" />
    203     </target>
    204     <target name="-jaxb_nix" unless="isWindows">
    205         <property name="xjc" value="${java.home}/../bin/xjc" />
    206     </target>
    207     <target name="jaxb" depends="init, -jaxb_win, -jaxb_nix" unless="jaxb.notRequired">
    208         <exec executable="${xjc}" failonerror="true">
    209             <arg value="-d"/>
    210             <arg value="${src.dir}"/>
    211             <arg value="-encoding"/>
    212             <arg value="UTF-8"/>
    213             <arg value="data_nodist/wms-cache.xsd"/>
    214         </exec>
    215     </target>
    216     <target name="compile" depends="init,javacc,jaxb">
     201    <target name="compile" depends="init,javacc">
    217202        <!-- COTS -->
    218203        <javac srcdir="${src.dir}" includes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**" nowarn="on" encoding="iso-8859-1"
    219204            destdir="build" target="1.7" source="1.7" debug="on" includeAntRuntime="false" createMissingPackageInfoClass="false">
  • deleted file data_nodist/wms-cache.xsd

    diff --git a/data_nodist/wms-cache.xsd b/data_nodist/wms-cache.xsd
    deleted file mode 100644
    index c6d50b8..0000000
    + -  
    1 <?xml version="1.0" encoding="UTF-8"?>
    2 <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://josm.openstreetmap.de/wms-cache-1.0"
    3         xmlns:tns="http://josm.openstreetmap.de/wms-cache-1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
    4         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5         xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
    6         elementFormDefault="qualified" jaxb:version="2.0">
    7        
    8         <annotation>
    9                 <appinfo>
    10                         <jaxb:schemaBindings>
    11                                 <jaxb:package name="org.openstreetmap.josm.data.imagery.types">
    12                                 </jaxb:package>
    13                                 <jaxb:nameXmlTransform>
    14                                         <jaxb:typeName suffix="Type" />
    15                                         <jaxb:elementName suffix="Type" />
    16                                 </jaxb:nameXmlTransform>
    17                         </jaxb:schemaBindings>
    18                         <jaxb:globalBindings>
    19                                 <jaxb:javaType name="java.util.Calendar" xmlType="date"
    20                                         parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
    21                                         printMethod="org.openstreetmap.josm.data.imagery.WmsCache.printDate" />
    22                         </jaxb:globalBindings>
    23                 </appinfo>
    24         </annotation>
    25 
    26         <element name="wms-cache">
    27                 <complexType>
    28                         <sequence>
    29                                 <element name="projection" type="tns:projection" minOccurs="0"
    30                                         maxOccurs="unbounded" />
    31                         </sequence>
    32                         <attribute name="tileSize" type="int" use="required" />
    33                         <attribute name="totalFileSize" type="int" use="required"/>
    34                 </complexType>
    35         </element>
    36        
    37         <complexType name="projection">
    38                 <sequence>
    39                         <element name="entry" type="tns:entry" minOccurs="0" maxOccurs="unbounded"/>
    40                 </sequence>
    41                 <attribute name="name" type="string"/>
    42                 <attribute name="cache-directory" type="string"/>
    43         </complexType>
    44 
    45         <complexType name="entry">
    46                 <sequence>
    47                         <element name="pixelPerDegree" type="double" />
    48                         <element name="east" type="double" />
    49                         <element name="north" type="double" />
    50                         <element name="lastUsed" type="date" />
    51                         <element name="lastModified" type="date" />
    52                         <element name="filename" type="string" />
    53                 </sequence>
    54         </complexType>
    55 </schema>
  • new file src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java

    diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
    new file mode 100644
    index 0000000..6c7e01a
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.gui.jmapviewer.interfaces;
     3
     4import java.util.Map;
     5
     6/**
     7 * Interface for template tile sources, @see TemplatedTMSTileSource
     8 *
     9 * @author Wiktor Niesiobędzki
     10 * @since TODO
     11 */
     12public interface TemplatedTileSource extends TileSource {
     13    /**
     14     *
     15     * @return headers to be sent with http requests
     16     */
     17    public Map<String, String> getHeaders();
     18}
  • src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java

    diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
    index 9c2518f..29d82db 100644
    a b import java.util.Map;  
    88import java.util.Map.Entry;
    99
    1010import org.openstreetmap.gui.jmapviewer.OsmMercator;
    11 
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     12import org.openstreetmap.josm.data.imagery.ImageryInfo;
     13import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
     14
     15/**
     16 * Class generalizing all tile based tile sources
     17 *
     18 * @author Wiktor Niesiobędzki
     19 *
     20 */
    1221public abstract class AbstractTMSTileSource extends AbstractTileSource {
    1322
    1423    protected String name;
    public abstract class AbstractTMSTileSource extends AbstractTileSource {  
    1928    protected int tileSize;
    2029    protected OsmMercator osmMercator;
    2130
     31    /**
     32     * Creates an instance based on TileSource information
     33     *
     34     * @param info description of the Tile Source
     35     */
    2236    public AbstractTMSTileSource(TileSourceInfo info) {
    2337        this.name = info.getName();
    2438        this.baseUrl = info.getUrl();
    public abstract class AbstractTMSTileSource extends AbstractTileSource {  
    3246        osmMercator = new OsmMercator(this.tileSize);
    3347    }
    3448
     49    /**
     50     * @return default tile size to use, when not set in Imagery Preferences
     51     */
     52    protected int getDefaultTileSize() {
     53        return OsmMercator.DEFAUL_TILE_SIZE;
     54    }
     55
    3556    @Override
    3657    public String getName() {
    3758        return name;
    public abstract class AbstractTMSTileSource extends AbstractTileSource {  
    5273        return 0;
    5374    }
    5475
     76    /**
     77     * @return image extension, used for URL creation
     78     */
    5579    public String getExtension() {
    5680        return "png";
    5781    }
    5882
    5983    /**
     84     * @param zoom level of the tile
     85     * @param tilex tile number in x axis
     86     * @param tiley tile number in y axis
     87     * @return String containg path part of URL of the tile
    6088     * @throws IOException when subclass cannot return the tile URL
    6189     */
    6290    public String getTilePath(int zoom, int tilex, int tiley) throws IOException {
    6391        return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension();
    6492    }
    6593
     94    /**
     95     * @return Base part of the URL of the tile source
     96     */
    6697    public String getBaseUrl() {
    6798        return this.baseUrl;
    6899    }
    public abstract class AbstractTMSTileSource extends AbstractTileSource {  
    87118     */
    88119    @Override
    89120    public int getTileSize() {
     121        if (tileSize <= 0) {
     122            return getDefaultTileSize();
     123        };
    90124        return tileSize;
    91125    }
    92126
    public abstract class AbstractTMSTileSource extends AbstractTileSource {  
    152186        return super.isNoTileAtZoom(headers, statusCode, content);
    153187    }
    154188
     189    /**
     190     * Converts imagery info to any of TMS supported TileSource
     191     * @param info
     192     * @return TileSource for specified info
     193     * @throws IllegalArgumentException
     194     */
     195    public static TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
     196        if (info.getImageryType() == ImageryType.TMS) {
     197            TemplatedTMSTileSource.checkUrl(info.getUrl());
     198            TMSTileSource t = new TemplatedTMSTileSource(info);
     199            info.setAttribution(t);
     200            return t;
     201        } else if (info.getImageryType() == ImageryType.BING)
     202            return new CachedAttributionBingAerialTileSource(info);
     203        else if (info.getImageryType() == ImageryType.SCANEX) {
     204            return new ScanexTileSource(info);
     205        }
     206        return null;
     207    }
     208
    155209    @Override
    156210    public Map<String, String> getMetadata(Map<String, List<String>> headers) {
    157211        Map<String, String> ret = new HashMap<>();
  • new file src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java

    diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java
    new file mode 100644
    index 0000000..42f9cf4
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.gui.jmapviewer.tilesources;
     3
     4import java.io.IOException;
     5import java.io.StringReader;
     6import java.net.URL;
     7import java.util.List;
     8import java.util.Scanner;
     9import java.util.concurrent.Callable;
     10
     11import org.openstreetmap.josm.Main;
     12import org.openstreetmap.josm.data.imagery.ImageryInfo;
     13import org.openstreetmap.josm.io.CacheCustomContent;
     14import org.openstreetmap.josm.io.UTFInputStreamReader;
     15import org.openstreetmap.josm.tools.Utils;
     16import org.xml.sax.InputSource;
     17
     18/**
     19 * Bing TileSource with cached attribution
     20 *
     21 * @author Wiktor Niesiobędzki
     22 * @since TODO
     23 *
     24 */
     25public class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
     26    /**
     27     * Creates tile source
     28     * @param info ImageryInfo description of this tile source
     29     */
     30    public CachedAttributionBingAerialTileSource(ImageryInfo info) {
     31        super(info);
     32    }
     33
     34    class BingAttributionData extends CacheCustomContent<IOException> {
     35
     36        public BingAttributionData() {
     37            super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
     38        }
     39
     40        @Override
     41        protected byte[] updateData() throws IOException {
     42            URL u = getAttributionUrl();
     43            try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
     44                String r = scanner.useDelimiter("\\A").next();
     45                Main.info("Successfully loaded Bing attribution data.");
     46                return r.getBytes("UTF-8");
     47            }
     48        }
     49    }
     50
     51    @Override
     52    protected Callable<List<Attribution>> getAttributionLoaderCallable() {
     53        return new Callable<List<Attribution>>() {
     54
     55            @Override
     56            public List<Attribution> call() throws Exception {
     57                BingAttributionData attributionLoader = new BingAttributionData();
     58                int waitTimeSec = 1;
     59                while (true) {
     60                    try {
     61                        String xml = attributionLoader.updateIfRequiredString();
     62                        return parseAttributionText(new InputSource(new StringReader((xml))));
     63                    } catch (IOException ex) {
     64                        Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
     65                        Thread.sleep(waitTimeSec * 1000L);
     66                        waitTimeSec *= 2;
     67                    }
     68                }
     69            }
     70        };
     71    }
     72}
     73
  • src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java

    diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
    index 4b43ff0..1e18c48 100644
    a b  
    11// License: GPL. For details, see Readme.txt file.
    22package org.openstreetmap.gui.jmapviewer.tilesources;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
    46import java.util.HashMap;
    57import java.util.Map;
    68import java.util.Random;
    79import java.util.regex.Matcher;
    810import java.util.regex.Pattern;
    911
    10 public class TemplatedTMSTileSource extends TMSTileSource {
     12import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
     13import org.openstreetmap.josm.data.imagery.ImageryInfo;
     14import org.openstreetmap.josm.tools.CheckParameterUtil;
     15
     16/**
     17 * Handles templated TMS Tile Source. Templated means, that some patterns within
     18 * URL gets substituted.
     19 *
     20 * Supported parameters
     21 * {zoom} - substituted with zoom level
     22 * {z} - as above
     23 * {NUMBER-zoom} - substituted with result of equation "NUMBER - zoom",
     24 *                  eg. {20-zoom} for zoom level 15 will result in 5 in this place
     25 * {zoom+number} - substituted with result of equation "zoom + number",
     26 *                 eg. {zoom+5} for zoom level 15 will result in 20.
     27 * {x} - substituted with X tile number
     28 * {y} - substituted with Y tile number
     29 * {!y} - substituted with Yahoo Y tile number
     30 * {-y} - substituted with reversed Y tile number
     31 * {switch:VAL_A,VAL_B,VAL_C,...} - substituted with one of VAL_A, VAL_B, VAL_C. Usually
     32 *                                  used to specify many tile servers
     33 * {header:(HEADER_NAME,HEADER_VALUE)} - sets the headers to be sent to tile server
     34 */
     35
     36public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTileSource {
    1137
    1238    private Random rand = null;
    1339    private String[] randomParts = null;
    1440    private Map<String, String> headers = new HashMap<>();
    1541
    16     public static final String COOKIE_HEADER   = "Cookie";
    17     public static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
    18     public static final String PATTERN_X       = "\\{x\\}";
    19     public static final String PATTERN_Y       = "\\{y\\}";
    20     public static final String PATTERN_Y_YAHOO = "\\{!y\\}";
    21     public static final String PATTERN_NEG_Y   = "\\{-y\\}";
    22     public static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
    23     public static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
     42    private static final String COOKIE_HEADER   = "Cookie";
     43    private static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
     44    private static final String PATTERN_X       = "\\{x\\}";
     45    private static final String PATTERN_Y       = "\\{y\\}";
     46    private static final String PATTERN_Y_YAHOO = "\\{!y\\}";
     47    private static final String PATTERN_NEG_Y   = "\\{-y\\}";
     48    private static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
     49    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
    2450
    25     public static final String[] ALL_PATTERNS = {
     51    private static final String[] ALL_PATTERNS = {
    2652        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y,
    2753        PATTERN_SWITCH
    2854    };
    2955
    30     public TemplatedTMSTileSource(TileSourceInfo info) {
     56    /**
     57     * Creates Templated TMS Tile Source based on ImageryInfo
     58     * @param info
     59     */
     60    public TemplatedTMSTileSource(ImageryInfo info) {
    3161        super(info);
    3262        if (info.getCookies() != null) {
    3363            headers.put(COOKIE_HEADER, info.getCookies());
    public class TemplatedTMSTileSource extends TMSTileSource {  
    83113        }
    84114        return r;
    85115    }
     116
     117    /**
     118     * Checks if url is acceptable by this Tile Source
     119     * @param url
     120     */
     121    public static void checkUrl(String url) {
     122        CheckParameterUtil.ensureParameterNotNull(url, "url");
     123        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
     124        while (m.find()) {
     125            boolean isSupportedPattern = false;
     126            for (String pattern : ALL_PATTERNS) {
     127                if (m.group().matches(pattern)) {
     128                    isSupportedPattern = true;
     129                    break;
     130                }
     131            }
     132            if (!isSupportedPattern) {
     133                throw new IllegalArgumentException(
     134                        tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
     135            }
     136        }
     137    }
    86138}
  • src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java

    diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java
    index 639a9d0..2aa48dd 100644
    a b package org.openstreetmap.gui.jmapviewer.tilesources;  
    33
    44import java.util.Map;
    55
    6 import org.openstreetmap.gui.jmapviewer.OsmMercator;
    7 
    86/**
    97 * Data class that keeps basic information about a tile source.
    108 *
    public class TileSourceInfo {  
    3230    protected String cookies;
    3331
    3432    /** tile size of the displayed tiles */
    35     private int tileSize = OsmMercator.DEFAUL_TILE_SIZE;
     33    private int tileSize = -1; // use default
    3634
    3735    /** mapping <header key, metadata key> */
    3836    protected Map<String, String> metadataHeaders;
    public class TileSourceInfo {  
    115113
    116114    /**
    117115     * Request tile size of this tile source
    118      * @return tile size provided by this tile source
     116     * @return tile size provided by this tile source, or -1 when default value should be used
    119117     */
    120118    public int getTileSize() {
    121119        return tileSize;
    public class TileSourceInfo {  
    126124     * @param tileSize
    127125     */
    128126    public void setTileSize(int tileSize) {
    129         if (tileSize <= 0) {
     127        if (tileSize == 0 || tileSize < -1) {
    130128            throw new AssertionError("Invalid tile size: " + tileSize);
    131129        }
    132130        this.tileSize = tileSize;
  • src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

    diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
    index f494675..d28251c 100644
    a b public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    189189                return;
    190190            }
    191191            // object not in cache, so submit work to separate thread
    192             getDownloadExecutor().execute(this);
     192            downloadJobExecutor.execute(this);
    193193        }
    194194    }
    195195
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    226226        return getUrl().getHost();
    227227    }
    228228
    229     /**
    230      * this needs to be non-static, so it can be overridden by subclasses
    231      */
    232     protected ThreadPoolExecutor getDownloadExecutor() {
    233         return downloadJobExecutor;
    234     }
    235 
    236229    public void run() {
    237230        final Thread currentThread = Thread.currentThread();
    238231        final String oldName = currentThread.getName();
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    466459     * cancels all outstanding tasks in the queue.
    467460     */
    468461    public void cancelOutstandingTasks() {
    469         ThreadPoolExecutor downloadExecutor = getDownloadExecutor();
    470         for(Runnable r: downloadExecutor.getQueue()) {
    471             if (downloadExecutor.remove(r)) {
    472                 if (r instanceof JCSCachedTileLoaderJob) {
     462        for(Runnable r: downloadJobExecutor.getQueue()) {
     463            if (downloadJobExecutor.remove(r) && r instanceof JCSCachedTileLoaderJob) {
    473464                ((JCSCachedTileLoaderJob<?, ?>) r).handleJobCancellation();
    474465            }
    475466        }
    476467    }
    477     }
    478468
    479469    /**
    480470     * Sets a job, that will be run, when job will finish execution
  • new file src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
    new file mode 100644
    index 0000000..24c04da
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.File;
     5import java.io.IOException;
     6import java.util.HashMap;
     7import java.util.Map;
     8
     9import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     11import org.openstreetmap.josm.Main;
     12import org.openstreetmap.josm.data.Version;
     13import org.openstreetmap.josm.data.preferences.StringProperty;
     14
     15/**
     16 * TileLoaderFactory creating JCS cached TileLoaders
     17 *
     18 * @author Wiktor Niesiobędzki
     19 * @since TODO
     20 *
     21 */
     22public abstract class CachedTileLoaderFactory implements TileLoaderFactory {
     23    /**
     24     * Keeps the cache directory where
     25     */
     26    public static final StringProperty PROP_TILECACHE_DIR = getTileCacheDir();
     27    private String cacheName;
     28
     29    /**
     30     * @param cacheName name of the cache region, that the created loader will use
     31     */
     32    public CachedTileLoaderFactory(String cacheName) {
     33        this.cacheName = cacheName;
     34    }
     35
     36    private static StringProperty getTileCacheDir() {
     37        String defPath = null;
     38        try {
     39            defPath = new File(Main.pref.getCacheDirectory(), "tiles").getAbsolutePath();
     40        } catch (SecurityException e) {
     41            Main.warn(e);
     42        }
     43        return new StringProperty("imagery.generic.loader.cachedir", defPath);
     44    }
     45
     46    @Override
     47    public TileLoader makeTileLoader(TileLoaderListener listener) {
     48        return makeTileLoader(listener, null);
     49    }
     50
     51    @Override
     52    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
     53        Map<String, String> headers = new HashMap<>();
     54        headers.put("User-Agent", Version.getInstance().getFullAgentString());
     55        headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
     56        if (inputHeaders != null)
     57            headers.putAll(inputHeaders);
     58
     59        try {
     60            return getLoader(listener, cacheName,
     61                    Main.pref.getInteger("socket.timeout.connect",15) * 1000,
     62                    Main.pref.getInteger("socket.timeout.read", 30) * 1000,
     63                    headers,
     64                    PROP_TILECACHE_DIR.get());
     65        } catch (IOException e) {
     66            Main.warn(e);
     67        }
     68        return null;
     69    }
     70
     71    protected abstract TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, int readTimeout, Map<String, String> headers, String cacheDir) throws IOException;
     72}
  • deleted file src/org/openstreetmap/josm/data/imagery/GeorefImage.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/GeorefImage.java b/src/org/openstreetmap/josm/data/imagery/GeorefImage.java
    deleted file mode 100644
    index 228ece2..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.data.imagery;
    3 
    4 import static org.openstreetmap.josm.tools.I18n.tr;
    5 
    6 import java.awt.Color;
    7 import java.awt.Font;
    8 import java.awt.Graphics;
    9 import java.awt.Image;
    10 import java.awt.Transparency;
    11 import java.awt.image.BufferedImage;
    12 import java.io.IOException;
    13 import java.io.ObjectInputStream;
    14 import java.io.ObjectOutputStream;
    15 import java.io.Serializable;
    16 import java.lang.ref.SoftReference;
    17 
    18 import javax.imageio.ImageIO;
    19 
    20 import org.openstreetmap.josm.data.coor.EastNorth;
    21 import org.openstreetmap.josm.gui.NavigatableComponent;
    22 import org.openstreetmap.josm.gui.layer.ImageryLayer;
    23 import org.openstreetmap.josm.gui.layer.WMSLayer;
    24 import org.openstreetmap.josm.tools.ImageProvider;
    25 
    26 public class GeorefImage implements Serializable {
    27     private static final long serialVersionUID = 1L;
    28 
    29     public enum State { IMAGE, NOT_IN_CACHE, FAILED, PARTLY_IN_CACHE}
    30 
    31     private WMSLayer layer;
    32     private State state;
    33 
    34     private BufferedImage image;
    35     private SoftReference<BufferedImage> reImg;
    36     private int xIndex;
    37     private int yIndex;
    38 
    39     private static final Color transparentColor = new Color(0,0,0,0);
    40     private Color fadeColor = transparentColor;
    41 
    42     public EastNorth getMin() {
    43         return layer.getEastNorth(xIndex, yIndex);
    44     }
    45 
    46     public EastNorth getMax() {
    47         return layer.getEastNorth(xIndex+1, yIndex+1);
    48     }
    49 
    50     public GeorefImage(WMSLayer layer) {
    51         this.layer = layer;
    52     }
    53 
    54     public void changePosition(int xIndex, int yIndex) {
    55         if (!equalPosition(xIndex, yIndex)) {
    56             this.xIndex = xIndex;
    57             this.yIndex = yIndex;
    58             this.image = null;
    59             flushResizedCachedInstance();
    60         }
    61     }
    62 
    63     public boolean equalPosition(int xIndex, int yIndex) {
    64         return this.xIndex == xIndex && this.yIndex == yIndex;
    65     }
    66 
    67     /**
    68      * Resets this image to initial state and release all resources being used.
    69      * @since 7132
    70      */
    71     public void resetImage() {
    72         if (image != null) {
    73             image.flush();
    74         }
    75         changeImage(null, null, null);
    76     }
    77 
    78     public void changeImage(State state, BufferedImage image, String errorMsg) {
    79         flushResizedCachedInstance();
    80         this.image = image;
    81         this.state = state;
    82         if (state == null)
    83             return;
    84         switch (state) {
    85         case FAILED:
    86             BufferedImage imgFailed = createImage();
    87             layer.drawErrorTile(imgFailed, errorMsg);
    88             this.image = imgFailed;
    89             break;
    90         case NOT_IN_CACHE:
    91             BufferedImage img = createImage();
    92             Graphics g = img.getGraphics();
    93             g.setColor(Color.GRAY);
    94             g.fillRect(0, 0, img.getWidth(), img.getHeight());
    95             Font font = g.getFont();
    96             Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);
    97             g.setFont(tempFont);
    98             g.setColor(Color.BLACK);
    99             String text = tr("Not in cache");
    100             g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, img.getHeight()/2);
    101             g.setFont(font);
    102             this.image = img;
    103             break;
    104         default:
    105             if (this.image != null) {
    106                 this.image = layer.sharpenImage(this.image);
    107             }
    108             break;
    109         }
    110     }
    111 
    112     private BufferedImage createImage() {
    113         return new BufferedImage(layer.getImageSize(), layer.getImageSize(), BufferedImage.TYPE_INT_RGB);
    114     }
    115 
    116     public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {
    117         if (getImage() == null)
    118             return false;
    119 
    120         if(!(this.xIndex == xIndex && this.yIndex == yIndex))
    121             return false;
    122 
    123         int left = layer.getImageX(xIndex);
    124         int bottom = layer.getImageY(yIndex);
    125         int width = layer.getImageWidth(xIndex);
    126         int height = layer.getImageHeight(yIndex);
    127 
    128         int x = left - leftEdge;
    129         int y = nc.getHeight() - (bottom - bottomEdge) - height;
    130 
    131         // This happens if you zoom outside the world
    132         if(width == 0 || height == 0)
    133             return false;
    134 
    135         // TODO: implement per-layer fade color
    136         Color newFadeColor;
    137         if (ImageryLayer.PROP_FADE_AMOUNT.get() == 0) {
    138             newFadeColor = transparentColor;
    139         } else {
    140             newFadeColor = ImageryLayer.getFadeColorWithAlpha();
    141         }
    142 
    143         BufferedImage img = reImg == null?null:reImg.get();
    144         if(img != null && img.getWidth() == width && img.getHeight() == height && fadeColor.equals(newFadeColor)) {
    145             g.drawImage(img, x, y, null);
    146             return true;
    147         }
    148 
    149         fadeColor = newFadeColor;
    150 
    151         boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;
    152 
    153         try {
    154             if(img != null) {
    155                 img.flush();
    156             }
    157             long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();
    158             // Notice that this value can get negative due to integer overflows
    159 
    160             int multipl = alphaChannel ? 4 : 3;
    161             // This happens when requesting images while zoomed out and then zooming in
    162             // Storing images this large in memory will certainly hang up JOSM. Luckily
    163             // traditional rendering is as fast at these zoom levels, so it's no loss.
    164             // Also prevent caching if we're out of memory soon
    165             if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {
    166                 fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
    167             } else {
    168                 // We haven't got a saved resized copy, so resize and cache it
    169                 img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);
    170                 img.getGraphics().drawImage(getImage(),
    171                         0, 0, width, height, // dest
    172                         0, 0, getImage().getWidth(null), getImage().getHeight(null), // src
    173                         null);
    174                 if (!alphaChannel) {
    175                     drawFadeRect(img.getGraphics(), 0, 0, width, height);
    176                 }
    177                 img.getGraphics().dispose();
    178                 g.drawImage(img, x, y, null);
    179                 reImg = new SoftReference<>(img);
    180             }
    181         } catch(Exception e) {
    182             fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);
    183         }
    184         return true;
    185     }
    186 
    187     private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height, boolean alphaChannel) {
    188         flushResizedCachedInstance();
    189         g.drawImage(
    190                 img, x, y, x + width, y + height,
    191                 0, 0, img.getWidth(null), img.getHeight(null),
    192                 null);
    193         if (!alphaChannel) { //FIXME: fading for layers with alpha channel currently is not supported
    194             drawFadeRect(g, x, y, width, height);
    195         }
    196     }
    197 
    198     private void drawFadeRect(Graphics g, int x, int y, int width, int height) {
    199         if (fadeColor != transparentColor) {
    200             g.setColor(fadeColor);
    201             g.fillRect(x, y, width, height);
    202         }
    203     }
    204 
    205     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    206         state = (State) in.readObject();
    207         boolean hasImage = in.readBoolean();
    208         if (hasImage) {
    209             image = ImageProvider.read(ImageIO.createImageInputStream(in), true, WMSLayer.PROP_ALPHA_CHANNEL.get());
    210         } else {
    211             in.readObject(); // read null from input stream
    212             image = null;
    213         }
    214     }
    215 
    216     private void writeObject(ObjectOutputStream out) throws IOException {
    217         out.writeObject(state);
    218         if(getImage() == null) {
    219             out.writeBoolean(false);
    220             out.writeObject(null);
    221         } else {
    222             out.writeBoolean(true);
    223             ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));
    224         }
    225     }
    226 
    227     public void flushResizedCachedInstance() {
    228         if (reImg != null) {
    229             BufferedImage img = reImg.get();
    230             if (img != null) {
    231                 img.flush();
    232             }
    233         }
    234         reImg = null;
    235     }
    236 
    237     public BufferedImage getImage() {
    238         return image;
    239     }
    240 
    241     public State getState() {
    242         return state;
    243     }
    244 
    245     public int getXIndex() {
    246         return xIndex;
    247     }
    248 
    249     public int getYIndex() {
    250         return yIndex;
    251     }
    252 
    253     public void setLayer(WMSLayer layer) {
    254         this.layer = layer;
    255     }
    256 }
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
    index b8d92c1..e63ae0f 100644
    a b import org.openstreetmap.josm.data.preferences.IntegerProperty;  
    2828 */
    2929public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache {
    3030
    31     private ICacheAccess<String, BufferedImageCacheEntry> cache;
    32     private int connectTimeout;
    33     private int readTimeout;
    34     private Map<String, String> headers;
    35     private TileLoaderListener listener;
     31    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
     32    protected final int connectTimeout;
     33    protected final int readTimeout;
     34    protected final Map<String, String> headers;
     35    protected final TileLoaderListener listener;
    3636    private static final String PREFERENCE_PREFIX   = "imagery.tms.cache.";
    3737    /**
    3838     * how many object on disk should be stored for TMS region. Average tile size is about 20kb
     39     *
     40     * 25000 is around 500MB under this assumptions
    3941     */
    40     public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions
     42    public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000);
    4143
    4244    /**
    4345     * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS
    public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa  
    5456     * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS
    5557     * and for TMS imagery
    5658     */
    57     private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getThreadPoolExecutor();
    58 
    59     private static ThreadPoolExecutor getThreadPoolExecutor() {
    60         return new ThreadPoolExecutor(
    61                 THREAD_LIMIT.get().intValue(), // keep the thread number constant
    62                 THREAD_LIMIT.get().intValue(), // do not this number of threads
    63                 30, // keepalive for thread
    64                 TimeUnit.SECONDS,
    65                 new HostLimitQueue(HOST_LIMIT.get().intValue()),
    66                 JCSCachedTileLoaderJob.getNamedThreadFactory("TMS downloader")
    67                 );
    68     }
     59    private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getNewThreadPoolExecutor("TMS downloader");
    6960
    7061    private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
    7162
    public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa  
    9081        this.listener = listener;
    9182    }
    9283
     84    /**
     85     * @param name name of the threads
     86     * @param workers number of worker thread to keep
     87     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue
     88     */
     89    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name, int workers) {
     90        return new ThreadPoolExecutor(
     91                workers, // keep the thread number constant
     92                workers, // do not this number of threads
     93                30, // keepalive for thread
     94                TimeUnit.SECONDS,
     95                new HostLimitQueue(HOST_LIMIT.get().intValue()),
     96                JCSCachedTileLoaderJob.getNamedThreadFactory(name)
     97                );
     98    }
     99
     100    /**
     101     * @param name name of threads
     102     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue, with default number of threads
     103     */
     104    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name) {
     105        return getNewThreadPoolExecutor(name, THREAD_LIMIT.get().intValue());
     106    }
     107
    93108    @Override
    94109    public TileJob createTileLoaderJob(Tile tile) {
    95110        return new TMSCachedTileLoaderJob(listener, tile, cache,
    96                 connectTimeout, readTimeout, headers, downloadExecutor);
     111                connectTimeout, readTimeout, headers, getDownloadExecutor());
    97112    }
    98113
    99114    @Override
    public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa  
    139154            }
    140155        }
    141156    }
     157
     158    /**
     159     * Sets the download executor that will be used to download tiles instead of default one.
     160     * You can use {@link #getNewThreadPoolExecutor} to create a new download executor with separate
     161     * queue from default.
     162     *
     163     * @param downloadExecutor
     164     */
     165    public void setDownloadExecutor(ThreadPoolExecutor downloadExecutor) {
     166        this.downloadExecutor = downloadExecutor;
     167    }
     168
     169    /**
     170     * @return download executor that is used by this factory
     171     */
     172    public ThreadPoolExecutor getDownloadExecutor() {
     173        return downloadExecutor;
     174    }
     175
    142176}
  • new file src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java b/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
    new file mode 100644
    index 0000000..3d13b31
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.io.IOException;
     7import java.text.DecimalFormat;
     8import java.text.DecimalFormatSymbols;
     9import java.text.NumberFormat;
     10import java.util.HashMap;
     11import java.util.Locale;
     12import java.util.Map;
     13import java.util.regex.Matcher;
     14import java.util.regex.Pattern;
     15
     16import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
     17import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
     18import org.openstreetmap.josm.Main;
     19import org.openstreetmap.josm.data.coor.EastNorth;
     20import org.openstreetmap.josm.data.coor.LatLon;
     21import org.openstreetmap.josm.data.projection.Projection;
     22import org.openstreetmap.josm.gui.layer.WMSLayer;
     23import org.openstreetmap.josm.tools.CheckParameterUtil;
     24
     25/**
     26 * Tile Source handling WMS providers
     27 *
     28 * @author Wiktor Niesiobędzki
     29 * @since TODO
     30 *
     31 */
     32public class TemplatedWMSTileSource extends TMSTileSource implements TemplatedTileSource {
     33    private Map<String, String> headers = new HashMap<>();
     34    private ImageryInfo info;
     35    private static final String COOKIE_HEADER   = "Cookie";
     36    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
     37    private static final String PATTERN_PROJ    = "\\{proj(\\([^})]+\\))?\\}";
     38    private static final String PATTERN_BBOX    = "\\{bbox\\}";
     39    private static final String PATTERN_W       = "\\{w\\}";
     40    private static final String PATTERN_S       = "\\{s\\}";
     41    private static final String PATTERN_E       = "\\{e\\}";
     42    private static final String PATTERN_N       = "\\{n\\}";
     43    private static final String PATTERN_WIDTH   = "\\{width\\}";
     44    private static final String PATTERN_HEIGHT  = "\\{height\\}";
     45
     46
     47    private static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
     48
     49    private static final String[] ALL_PATTERNS = {
     50        PATTERN_HEADER, PATTERN_PROJ, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT
     51    };
     52
     53    /**
     54     * Creates a tile source based on imagery info
     55     * @param info
     56     */
     57    public TemplatedWMSTileSource(ImageryInfo info) {
     58        super(info);
     59        this.info = info;
     60        if (info.getCookies() != null) {
     61            headers.put(COOKIE_HEADER, info.getCookies());
     62        }
     63
     64        handleTemplate();
     65    }
     66
     67    private void handleTemplate() {
     68        // Capturing group pattern on switch values
     69        Pattern pattern = Pattern.compile(PATTERN_HEADER);
     70        StringBuffer output = new StringBuffer();
     71        Matcher matcher = pattern.matcher(this.baseUrl);
     72        while (matcher.find()) {
     73            headers.put(matcher.group(1),matcher.group(2));
     74            matcher.appendReplacement(output, "");
     75        }
     76        matcher.appendTail(output);
     77        this.baseUrl = output.toString();
     78    }
     79
     80    @Override
     81    protected int getDefaultTileSize() {
     82        return WMSLayer.PROP_IMAGE_SIZE.get();
     83    }
     84
     85    @Override
     86    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
     87        Projection myProj = Main.getProjection();
     88        String myProjCode = Main.getProjection().toCode();
     89
     90        EastNorth sw = myProj.latlon2eastNorth(new LatLon(
     91                tileYToLat(tiley+1, zoom),
     92                tileXToLon(tilex, zoom)
     93                ));
     94        EastNorth ne = myProj.latlon2eastNorth(new LatLon(
     95                tileYToLat(tiley, zoom),
     96                tileXToLon(tilex+1, zoom)
     97                ));
     98
     99        double w = sw.getX();
     100        double s = sw.getY();
     101
     102        double e = ne.getX();
     103        double n = ne.getY();
     104
     105        if (!info.getServerProjections().contains(myProjCode) && "EPSG:3857".equals(Main.getProjection().toCode())) {
     106            LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
     107            LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
     108            myProjCode = "EPSG:4326";
     109            s = swll.lat();
     110            w = swll.lon();
     111            n = nell.lat();
     112            e = nell.lon();
     113        }
     114        if ("EPSG:4326".equals(myProjCode) && !info.getServerProjections().contains(myProjCode) && info.getServerProjections().contains("CRS:84")) {
     115            myProjCode = "CRS:84";
     116        }
     117
     118        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
     119        //
     120        // Background:
     121        //
     122        // bbox=x_min,y_min,x_max,y_max
     123        //
     124        //      SRS=... is WMS 1.1.1
     125        //      CRS=... is WMS 1.3.0
     126        //
     127        // The difference:
     128        //      For SRS x is east-west and y is north-south
     129        //      For CRS x and y are as specified by the EPSG
     130        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
     131        //          For most other EPSG code there seems to be no difference.
     132        // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326
     133        boolean switchLatLon = false;
     134        if (baseUrl.toLowerCase().contains("crs=epsg:4326")) {
     135            switchLatLon = true;
     136        } else if (baseUrl.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProjCode)) {
     137            switchLatLon = true;
     138        }
     139        String bbox;
     140        if (switchLatLon) {
     141            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));
     142        } else {
     143            bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));
     144        }
     145        return baseUrl.
     146                replaceAll(PATTERN_PROJ,    myProjCode)
     147                .replaceAll(PATTERN_BBOX,   bbox)
     148                .replaceAll(PATTERN_W,      latLonFormat.format(w))
     149                .replaceAll(PATTERN_S,      latLonFormat.format(s))
     150                .replaceAll(PATTERN_E,      latLonFormat.format(e))
     151                .replaceAll(PATTERN_N,      latLonFormat.format(n))
     152                .replaceAll(PATTERN_WIDTH,  String.valueOf(getTileSize()))
     153                .replaceAll(PATTERN_HEIGHT, String.valueOf(getTileSize()))
     154                .replace(" ", "%20");
     155    }
     156
     157    /**
     158     * Checks if url is acceptable by this Tile Source
     159     * @param url
     160     */
     161    public static void checkUrl(String url) {
     162        CheckParameterUtil.ensureParameterNotNull(url, "url");
     163        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
     164        while (m.find()) {
     165            boolean isSupportedPattern = false;
     166            for (String pattern : ALL_PATTERNS) {
     167                if (m.group().matches(pattern)) {
     168                    isSupportedPattern = true;
     169                    break;
     170                }
     171            }
     172            if (!isSupportedPattern) {
     173                throw new IllegalArgumentException(
     174                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
     175            }
     176        }
     177    }
     178
     179    @Override
     180    public Map<String, String> getHeaders() {
     181        return headers;
     182    }
     183}
  • new file src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
    new file mode 100644
    index 0000000..2c1dd82
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.util.Map;
     5
     6import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     7import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     8
     9/**
     10 * Factory creating TileLoaders for layers
     11 *
     12 * @author Wiktor Niesiobędzki
     13 * @since TODO
     14 *
     15 */
     16public interface TileLoaderFactory {
     17
     18    /**
     19     * @param listener that will be notified, when tile has finished loading
     20     * @return TileLoader that notifies specified listener
     21     */
     22    TileLoader makeTileLoader(TileLoaderListener listener);
     23
     24    /**
     25     * @param listener that will be notified, when tile has finished loading
     26     * @param headers that will be sent with requests to TileSource
     27     * @return TileLoader that uses both of above
     28     */
     29    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
     30
     31}
  • new file src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
    new file mode 100644
    index 0000000..45b0970
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.IOException;
     5import java.util.Map;
     6
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     9import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     10import org.openstreetmap.josm.data.preferences.IntegerProperty;
     11
     12/**
     13 * Tileloader for WMS based imagery. It is separate to use different ThreadPoolExecutor, as we want
     14 * to define number of simultaneous downloads for WMS separately
     15 *
     16 * @author Wiktor Niesiobędzki
     17 * @since TODO
     18 *
     19 */
     20public class WMSCachedTileLoader extends TMSCachedTileLoader {
     21
     22    /** limit of concurrent connections to WMS tile source (per source) */
     23    public static IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
     24
     25    /**
     26     * Creates a TileLoader with separate WMS downloader.
     27     *
     28     * @param listener that will be notified when tile is loaded
     29     * @param name name of the cache region
     30     * @param connectTimeout to tile source
     31     * @param readTimeout from tile source
     32     * @param headers to be sent with requests
     33     * @param cacheDir place to store the cache
     34     * @throws IOException when there is a problem creating cache repository
     35     */
     36    public WMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout,
     37            Map<String, String> headers, String cacheDir) throws IOException {
     38
     39        super(listener, name, connectTimeout, readTimeout, headers, cacheDir);
     40        setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS downloader", THREAD_LIMIT.get()));
     41    }
     42
     43    @Override
     44    public TileJob createTileLoaderJob(Tile tile) {
     45        return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor());
     46    }
     47}
  • new file src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
    new file mode 100644
    index 0000000..c9f5356
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.util.List;
     5import java.util.Map;
     6import java.util.concurrent.ThreadPoolExecutor;
     7
     8import org.apache.commons.jcs.access.behavior.ICacheAccess;
     9import org.openstreetmap.gui.jmapviewer.Tile;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     11import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     12
     13/**
     14 * Separate class to handle WMS jobs, as it needs to react differently to HTTP response codes from WMS server
     15 *
     16 * @author Wiktor Niesiobędzki
     17 * @since TODO
     18 *
     19 */
     20public class WMSCachedTileLoaderJob extends TMSCachedTileLoaderJob {
     21
     22    /**
     23     * Creates a job - that will download specific tile
     24     * @param listener will be notified, when tile has loaded
     25     * @param tile to load
     26     * @param cache to use (get/put)
     27     * @param connectTimeout to tile source
     28     * @param readTimeout to tile source
     29     * @param headers to be sent with request
     30     * @param downloadExecutor that will execute the download task (if needed)
     31     */
     32    public WMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
     33            ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout,
     34            Map<String, String> headers, ThreadPoolExecutor downloadExecutor) {
     35        super(listener, tile, cache, connectTimeout, readTimeout, headers, downloadExecutor);
     36    }
     37
     38    /**
     39     * If 404 is returned from WMS server, treat this as situation as erroneus
     40     */
     41    @Override
     42    protected boolean handleNotFound() {
     43        return false;
     44    }
     45
     46    /**
     47     * Do not try to cache empty responses
     48     */
     49    @Override
     50    protected boolean cacheAsEmpty(Map<String, List<String>> headers, int statusCode, byte[] content) {
     51        if (statusCode < 100 || statusCode > 400) {
     52            // cache any errors
     53            return true;
     54        }
     55        return false;
     56    }
     57
     58}
  • deleted file src/org/openstreetmap/josm/data/imagery/WmsCache.java

    diff --git a/src/org/openstreetmap/josm/data/imagery/WmsCache.java b/src/org/openstreetmap/josm/data/imagery/WmsCache.java
    deleted file mode 100644
    index 1223fdf..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.data.imagery;
    3 
    4 import java.awt.Graphics2D;
    5 import java.awt.image.BufferedImage;
    6 import java.io.BufferedOutputStream;
    7 import java.io.File;
    8 import java.io.FileInputStream;
    9 import java.io.FileNotFoundException;
    10 import java.io.FileOutputStream;
    11 import java.io.IOException;
    12 import java.io.InputStream;
    13 import java.io.OutputStream;
    14 import java.lang.ref.SoftReference;
    15 import java.net.URLConnection;
    16 import java.util.ArrayList;
    17 import java.util.Calendar;
    18 import java.util.Collections;
    19 import java.util.Comparator;
    20 import java.util.HashMap;
    21 import java.util.HashSet;
    22 import java.util.Iterator;
    23 import java.util.List;
    24 import java.util.Map;
    25 import java.util.Properties;
    26 import java.util.Set;
    27 
    28 import javax.imageio.ImageIO;
    29 import javax.xml.bind.JAXBContext;
    30 import javax.xml.bind.Marshaller;
    31 import javax.xml.bind.Unmarshaller;
    32 
    33 import org.openstreetmap.josm.Main;
    34 import org.openstreetmap.josm.data.ProjectionBounds;
    35 import org.openstreetmap.josm.data.SystemOfMeasurement;
    36 import org.openstreetmap.josm.data.coor.EastNorth;
    37 import org.openstreetmap.josm.data.coor.LatLon;
    38 import org.openstreetmap.josm.data.imagery.types.EntryType;
    39 import org.openstreetmap.josm.data.imagery.types.ProjectionType;
    40 import org.openstreetmap.josm.data.imagery.types.WmsCacheType;
    41 import org.openstreetmap.josm.data.preferences.StringProperty;
    42 import org.openstreetmap.josm.data.projection.Projection;
    43 import org.openstreetmap.josm.gui.layer.WMSLayer;
    44 import org.openstreetmap.josm.tools.ImageProvider;
    45 import org.openstreetmap.josm.tools.Utils;
    46 import org.openstreetmap.josm.tools.date.DateUtils;
    47 
    48 public class WmsCache {
    49     //TODO Property for maximum cache size
    50     //TODO Property for maximum age of tile, automatically remove old tiles
    51     //TODO Measure time for partially loading from cache, compare with time to download tile. If slower, disable partial cache
    52     //TODO Do loading from partial cache and downloading at the same time, don't wait for partial cache to load
    53 
    54     private static final StringProperty PROP_CACHE_PATH = new StringProperty("imagery.wms-cache.path", "wms");
    55     private static final String INDEX_FILENAME = "index.xml";
    56     private static final String LAYERS_INDEX_FILENAME = "layers.properties";
    57 
    58     private static class CacheEntry {
    59         private final double pixelPerDegree;
    60         private final double east;
    61         private final double north;
    62         private final ProjectionBounds bounds;
    63         private final String filename;
    64 
    65         private long lastUsed;
    66         private long lastModified;
    67 
    68         CacheEntry(double pixelPerDegree, double east, double north, int tileSize, String filename) {
    69             this.pixelPerDegree = pixelPerDegree;
    70             this.east = east;
    71             this.north = north;
    72             this.bounds = new ProjectionBounds(east, north, east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree);
    73             this.filename = filename;
    74         }
    75 
    76         @Override
    77         public String toString() {
    78             return "CacheEntry [pixelPerDegree=" + pixelPerDegree + ", east=" + east + ", north=" + north + ", bounds="
    79                     + bounds + ", filename=" + filename + ", lastUsed=" + lastUsed + ", lastModified=" + lastModified
    80                     + "]";
    81         }
    82     }
    83 
    84     private static class ProjectionEntries {
    85         private final String projection;
    86         private final String cacheDirectory;
    87         private final List<CacheEntry> entries = new ArrayList<>();
    88 
    89         ProjectionEntries(String projection, String cacheDirectory) {
    90             this.projection = projection;
    91             this.cacheDirectory = cacheDirectory;
    92         }
    93     }
    94 
    95     private final Map<String, ProjectionEntries> entries = new HashMap<>();
    96     private final File cacheDir;
    97     private final int tileSize; // Should be always 500
    98     private int totalFileSize;
    99     private boolean totalFileSizeDirty; // Some file was missing - size needs to be recalculated
    100     // No need for hashCode/equals on CacheEntry, object identity is enough. Comparing by values can lead to error - CacheEntry for wrong projection could be found
    101     private Map<CacheEntry, SoftReference<BufferedImage>> memoryCache = new HashMap<>();
    102     private Set<ProjectionBounds> areaToCache;
    103 
    104     protected String cacheDirPath() {
    105         String cPath = PROP_CACHE_PATH.get();
    106         if (!(new File(cPath).isAbsolute())) {
    107             cPath = Main.pref.getCacheDirectory() + File.separator + cPath;
    108         }
    109         return cPath;
    110     }
    111 
    112     public WmsCache(String url, int tileSize) {
    113         File globalCacheDir = new File(cacheDirPath());
    114         if (!globalCacheDir.mkdirs()) {
    115             Main.warn("Unable to create global cache directory: "+globalCacheDir.getAbsolutePath());
    116         }
    117         cacheDir = new File(globalCacheDir, getCacheDirectory(url));
    118         cacheDir.mkdirs();
    119         this.tileSize = tileSize;
    120     }
    121 
    122     private String getCacheDirectory(String url) {
    123         String cacheDirName = null;
    124         Properties layersIndex = new Properties();
    125         File layerIndexFile = new File(cacheDirPath(), LAYERS_INDEX_FILENAME);
    126         try (InputStream fis = new FileInputStream(layerIndexFile)) {
    127             layersIndex.load(fis);
    128         } catch (FileNotFoundException e) {
    129             Main.error("Unable to load layers index for wms cache (file " + layerIndexFile + " not found)");
    130         } catch (IOException e) {
    131             Main.error("Unable to load layers index for wms cache");
    132             Main.error(e);
    133         }
    134 
    135         for (Object propKey: layersIndex.keySet()) {
    136             String s = (String)propKey;
    137             if (url.equals(layersIndex.getProperty(s))) {
    138                 cacheDirName = s;
    139                 break;
    140             }
    141         }
    142 
    143         if (cacheDirName == null) {
    144             int counter = 0;
    145             while (true) {
    146                 counter++;
    147                 if (!layersIndex.keySet().contains(String.valueOf(counter))) {
    148                     break;
    149                 }
    150             }
    151             cacheDirName = String.valueOf(counter);
    152             layersIndex.setProperty(cacheDirName, url);
    153             try (OutputStream fos = new FileOutputStream(layerIndexFile)) {
    154                 layersIndex.store(fos, "");
    155             } catch (IOException e) {
    156                 Main.error("Unable to save layer index for wms cache");
    157                 Main.error(e);
    158             }
    159         }
    160 
    161         return cacheDirName;
    162     }
    163 
    164     private ProjectionEntries getProjectionEntries(Projection projection) {
    165         return getProjectionEntries(projection.toCode(), projection.getCacheDirectoryName());
    166     }
    167 
    168     private ProjectionEntries getProjectionEntries(String projection, String cacheDirectory) {
    169         ProjectionEntries result = entries.get(projection);
    170         if (result == null) {
    171             result = new ProjectionEntries(projection, cacheDirectory);
    172             entries.put(projection, result);
    173         }
    174 
    175         return result;
    176     }
    177 
    178     public synchronized void loadIndex() {
    179         File indexFile = new File(cacheDir, INDEX_FILENAME);
    180         try {
    181             JAXBContext context = JAXBContext.newInstance(
    182                     WmsCacheType.class.getPackage().getName(),
    183                     WmsCacheType.class.getClassLoader());
    184             Unmarshaller unmarshaller = context.createUnmarshaller();
    185             WmsCacheType cacheEntries;
    186             try (InputStream is = new FileInputStream(indexFile)) {
    187                 cacheEntries = (WmsCacheType)unmarshaller.unmarshal(is);
    188             }
    189             totalFileSize = cacheEntries.getTotalFileSize();
    190             if (cacheEntries.getTileSize() != tileSize) {
    191                 Main.info("Cache created with different tileSize, cache will be discarded");
    192                 return;
    193             }
    194             for (ProjectionType projectionType: cacheEntries.getProjection()) {
    195                 ProjectionEntries projection = getProjectionEntries(projectionType.getName(), projectionType.getCacheDirectory());
    196                 for (EntryType entry: projectionType.getEntry()) {
    197                     CacheEntry ce = new CacheEntry(entry.getPixelPerDegree(), entry.getEast(), entry.getNorth(), tileSize, entry.getFilename());
    198                     ce.lastUsed = entry.getLastUsed().getTimeInMillis();
    199                     ce.lastModified = entry.getLastModified().getTimeInMillis();
    200                     projection.entries.add(ce);
    201                 }
    202             }
    203         } catch (Exception e) {
    204             if (indexFile.exists()) {
    205                 Main.error(e);
    206                 Main.info("Unable to load index for wms-cache, new file will be created");
    207             } else {
    208                 Main.info("Index for wms-cache doesn't exist, new file will be created");
    209             }
    210         }
    211 
    212         removeNonReferencedFiles();
    213     }
    214 
    215     private void removeNonReferencedFiles() {
    216 
    217         Set<String> usedProjections = new HashSet<>();
    218 
    219         for (ProjectionEntries projectionEntries: entries.values()) {
    220 
    221             usedProjections.add(projectionEntries.cacheDirectory);
    222 
    223             File projectionDir = new File(cacheDir, projectionEntries.cacheDirectory);
    224             if (projectionDir.exists()) {
    225                 Set<String> referencedFiles = new HashSet<>();
    226 
    227                 for (CacheEntry ce: projectionEntries.entries) {
    228                     referencedFiles.add(ce.filename);
    229                 }
    230 
    231                 File[] files = projectionDir.listFiles();
    232                 if (files != null) {
    233                     for (File file: files) {
    234                         if (!referencedFiles.contains(file.getName()) && !file.delete()) {
    235                             Main.warn("Unable to delete file: "+file.getAbsolutePath());
    236                         }
    237                     }
    238                 }
    239             }
    240         }
    241 
    242         File[] files = cacheDir.listFiles();
    243         if (files != null) {
    244             for (File projectionDir: files) {
    245                 if (projectionDir.isDirectory() && !usedProjections.contains(projectionDir.getName())) {
    246                     Utils.deleteDirectory(projectionDir);
    247                 }
    248             }
    249         }
    250     }
    251 
    252     private int calculateTotalFileSize() {
    253         int result = 0;
    254         for (ProjectionEntries projectionEntries: entries.values()) {
    255             Iterator<CacheEntry> it = projectionEntries.entries.iterator();
    256             while (it.hasNext()) {
    257                 CacheEntry entry = it.next();
    258                 File imageFile = getImageFile(projectionEntries, entry);
    259                 if (!imageFile.exists()) {
    260                     it.remove();
    261                 } else {
    262                     result += imageFile.length();
    263                 }
    264             }
    265         }
    266         return result;
    267     }
    268 
    269     public synchronized void saveIndex() {
    270         WmsCacheType index = new WmsCacheType();
    271 
    272         if (totalFileSizeDirty) {
    273             totalFileSize = calculateTotalFileSize();
    274         }
    275 
    276         index.setTileSize(tileSize);
    277         index.setTotalFileSize(totalFileSize);
    278         for (ProjectionEntries projectionEntries: entries.values()) {
    279             if (!projectionEntries.entries.isEmpty()) {
    280                 ProjectionType projectionType = new ProjectionType();
    281                 projectionType.setName(projectionEntries.projection);
    282                 projectionType.setCacheDirectory(projectionEntries.cacheDirectory);
    283                 index.getProjection().add(projectionType);
    284                 for (CacheEntry ce: projectionEntries.entries) {
    285                     EntryType entry = new EntryType();
    286                     entry.setPixelPerDegree(ce.pixelPerDegree);
    287                     entry.setEast(ce.east);
    288                     entry.setNorth(ce.north);
    289                     Calendar c = Calendar.getInstance();
    290                     c.setTimeInMillis(ce.lastUsed);
    291                     entry.setLastUsed(c);
    292                     c = Calendar.getInstance();
    293                     c.setTimeInMillis(ce.lastModified);
    294                     entry.setLastModified(c);
    295                     entry.setFilename(ce.filename);
    296                     projectionType.getEntry().add(entry);
    297                 }
    298             }
    299         }
    300         try {
    301             JAXBContext context = JAXBContext.newInstance(
    302                     WmsCacheType.class.getPackage().getName(),
    303                     WmsCacheType.class.getClassLoader());
    304             Marshaller marshaller = context.createMarshaller();
    305             try (OutputStream fos = new FileOutputStream(new File(cacheDir, INDEX_FILENAME))) {
    306                 marshaller.marshal(index, fos);
    307             }
    308         } catch (Exception e) {
    309             Main.error("Failed to save wms-cache file");
    310             Main.error(e);
    311         }
    312     }
    313 
    314     private File getImageFile(ProjectionEntries projection, CacheEntry entry) {
    315         return new File(cacheDir, projection.cacheDirectory + "/" + entry.filename);
    316     }
    317 
    318     private BufferedImage loadImage(ProjectionEntries projectionEntries, CacheEntry entry, boolean enforceTransparency) throws IOException {
    319         synchronized (this) {
    320             entry.lastUsed = System.currentTimeMillis();
    321 
    322             SoftReference<BufferedImage> memCache = memoryCache.get(entry);
    323             if (memCache != null) {
    324                 BufferedImage result = memCache.get();
    325                 if (result != null) {
    326                     if (enforceTransparency == ImageProvider.isTransparencyForced(result)) {
    327                         return result;
    328                     } else if (Main.isDebugEnabled()) {
    329                         Main.debug("Skipping "+entry+" from memory cache (transparency enforcement)");
    330                     }
    331                 }
    332             }
    333         }
    334 
    335         try {
    336             // Reading can't be in synchronized section, it's too slow
    337             BufferedImage result = ImageProvider.read(getImageFile(projectionEntries, entry), true, enforceTransparency);
    338             synchronized (this) {
    339                 if (result == null) {
    340                     projectionEntries.entries.remove(entry);
    341                     totalFileSizeDirty = true;
    342                 }
    343                 return result;
    344             }
    345         } catch (IOException e) {
    346             synchronized (this) {
    347                 projectionEntries.entries.remove(entry);
    348                 totalFileSizeDirty = true;
    349                 throw e;
    350             }
    351         }
    352     }
    353 
    354     private CacheEntry findEntry(ProjectionEntries projectionEntries, double pixelPerDegree, double east, double north) {
    355         for (CacheEntry entry: projectionEntries.entries) {
    356             if (Utils.equalsEpsilon(entry.pixelPerDegree, pixelPerDegree)
    357                     && Utils.equalsEpsilon(entry.east, east) && Utils.equalsEpsilon(entry.north, north))
    358                 return entry;
    359         }
    360         return null;
    361     }
    362 
    363     public synchronized boolean hasExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
    364         ProjectionEntries projectionEntries = getProjectionEntries(projection);
    365         return findEntry(projectionEntries, pixelPerDegree, east, north) != null;
    366     }
    367 
    368     public BufferedImage getExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
    369         CacheEntry entry = null;
    370         ProjectionEntries projectionEntries = null;
    371         synchronized (this) {
    372             projectionEntries = getProjectionEntries(projection);
    373             entry = findEntry(projectionEntries, pixelPerDegree, east, north);
    374         }
    375         if (entry != null) {
    376             try {
    377                 return loadImage(projectionEntries, entry, WMSLayer.PROP_ALPHA_CHANNEL.get());
    378             } catch (IOException e) {
    379                 Main.error("Unable to load file from wms cache");
    380                 Main.error(e);
    381                 return null;
    382             }
    383         }
    384         return null;
    385     }
    386 
    387     public BufferedImage getPartialMatch(Projection projection, double pixelPerDegree, double east, double north) {
    388         ProjectionEntries projectionEntries;
    389         List<CacheEntry> matches;
    390         synchronized (this) {
    391             matches = new ArrayList<>();
    392 
    393             double minPPD = pixelPerDegree / 5;
    394             double maxPPD = pixelPerDegree * 5;
    395             projectionEntries = getProjectionEntries(projection);
    396 
    397             double size2 = tileSize / pixelPerDegree;
    398             double border = tileSize * 0.01; // Make sure not to load neighboring tiles that intersects this tile only slightly
    399             ProjectionBounds bounds = new ProjectionBounds(east + border, north + border,
    400                     east + size2 - border, north + size2 - border);
    401 
    402             //TODO Do not load tile if it is completely overlapped by other tile with better ppd
    403             for (CacheEntry entry: projectionEntries.entries) {
    404                 if (entry.pixelPerDegree >= minPPD && entry.pixelPerDegree <= maxPPD && entry.bounds.intersects(bounds)) {
    405                     entry.lastUsed = System.currentTimeMillis();
    406                     matches.add(entry);
    407                 }
    408             }
    409 
    410             if (matches.isEmpty())
    411                 return null;
    412 
    413             Collections.sort(matches, new Comparator<CacheEntry>() {
    414                 @Override
    415                 public int compare(CacheEntry o1, CacheEntry o2) {
    416                     return Double.compare(o2.pixelPerDegree, o1.pixelPerDegree);
    417                 }
    418             });
    419         }
    420 
    421         // Use alpha layer only when enabled on wms layer
    422         boolean alpha = WMSLayer.PROP_ALPHA_CHANNEL.get();
    423         BufferedImage result = new BufferedImage(tileSize, tileSize,
    424                 alpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
    425         Graphics2D g = result.createGraphics();
    426 
    427         boolean drawAtLeastOnce = false;
    428         Map<CacheEntry, SoftReference<BufferedImage>> localCache = new HashMap<>();
    429         for (CacheEntry ce: matches) {
    430             BufferedImage img;
    431             try {
    432                 // Enforce transparency only when alpha enabled on wms layer too
    433                 img = loadImage(projectionEntries, ce, alpha);
    434                 localCache.put(ce, new SoftReference<>(img));
    435             } catch (IOException e) {
    436                 continue;
    437             }
    438 
    439             drawAtLeastOnce = true;
    440 
    441             int xDiff = (int)((ce.east - east) * pixelPerDegree);
    442             int yDiff = (int)((ce.north - north) * pixelPerDegree);
    443             int size = (int)(pixelPerDegree / ce.pixelPerDegree  * tileSize);
    444 
    445             int x = xDiff;
    446             int y = -size + tileSize - yDiff;
    447 
    448             g.drawImage(img, x, y, size, size, null);
    449         }
    450 
    451         if (drawAtLeastOnce) {
    452             synchronized (this) {
    453                 memoryCache.putAll(localCache);
    454             }
    455             return result;
    456         } else
    457             return null;
    458     }
    459 
    460     private String generateFileName(ProjectionEntries projectionEntries, double pixelPerDegree, Projection projection, double east, double north, String mimeType) {
    461         LatLon ll1 = projection.eastNorth2latlon(new EastNorth(east, north));
    462         LatLon ll2 = projection.eastNorth2latlon(new EastNorth(east + 100 / pixelPerDegree, north));
    463         LatLon ll3 = projection.eastNorth2latlon(new EastNorth(east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree));
    464 
    465         double deltaLat = Math.abs(ll3.lat() - ll1.lat());
    466         double deltaLon = Math.abs(ll3.lon() - ll1.lon());
    467         int precisionLat = Math.max(0, -(int)Math.ceil(Math.log10(deltaLat)) + 1);
    468         int precisionLon = Math.max(0, -(int)Math.ceil(Math.log10(deltaLon)) + 1);
    469 
    470         String zoom = SystemOfMeasurement.METRIC.getDistText(ll1.greatCircleDistance(ll2));
    471         String extension = "dat";
    472         if (mimeType != null) {
    473             switch(mimeType) {
    474             case "image/jpeg":
    475             case "image/jpg":
    476                 extension = "jpg";
    477                 break;
    478             case "image/png":
    479                 extension = "png";
    480                 break;
    481             case "image/gif":
    482                 extension = "gif";
    483                 break;
    484             default:
    485                 Main.warn("Unrecognized MIME type: "+mimeType);
    486             }
    487         }
    488 
    489         int counter = 0;
    490         FILENAME_LOOP:
    491             while (true) {
    492                 String result = String.format("%s_%." + precisionLat + "f_%." + precisionLon +"f%s.%s", zoom, ll1.lat(), ll1.lon(), counter==0?"":"_" + counter, extension);
    493                 for (CacheEntry entry: projectionEntries.entries) {
    494                     if (entry.filename.equals(result)) {
    495                         counter++;
    496                         continue FILENAME_LOOP;
    497                     }
    498                 }
    499                 return result;
    500             }
    501     }
    502 
    503     /**
    504      *
    505      * @param img Used only when overlapping is used, when not used, used raw from imageData
    506      * @param imageData
    507      * @param projection
    508      * @param pixelPerDegree
    509      * @param east
    510      * @param north
    511      * @throws IOException
    512      */
    513     public synchronized void saveToCache(BufferedImage img, InputStream imageData, Projection projection, double pixelPerDegree, double east, double north) throws IOException {
    514         ProjectionEntries projectionEntries = getProjectionEntries(projection);
    515         CacheEntry entry = findEntry(projectionEntries, pixelPerDegree, east, north);
    516         File imageFile;
    517         if (entry == null) {
    518 
    519             String mimeType;
    520             if (img != null) {
    521                 mimeType = "image/png";
    522             } else {
    523                 mimeType = URLConnection.guessContentTypeFromStream(imageData);
    524             }
    525             entry = new CacheEntry(pixelPerDegree, east, north, tileSize,generateFileName(projectionEntries, pixelPerDegree, projection, east, north, mimeType));
    526             entry.lastUsed = System.currentTimeMillis();
    527             entry.lastModified = entry.lastUsed;
    528             projectionEntries.entries.add(entry);
    529             imageFile = getImageFile(projectionEntries, entry);
    530         } else {
    531             imageFile = getImageFile(projectionEntries, entry);
    532             totalFileSize -= imageFile.length();
    533         }
    534 
    535         if (!imageFile.getParentFile().mkdirs()) {
    536             Main.warn("Unable to create parent directory: "+imageFile.getParentFile().getAbsolutePath());
    537         }
    538 
    539         if (img != null) {
    540             BufferedImage copy = new BufferedImage(tileSize, tileSize, img.getType());
    541             copy.createGraphics().drawImage(img, 0, 0, tileSize, tileSize, 0, img.getHeight() - tileSize, tileSize, img.getHeight(), null);
    542             ImageIO.write(copy, "png", imageFile);
    543             totalFileSize += imageFile.length();
    544         } else {
    545             try (OutputStream os = new BufferedOutputStream(new FileOutputStream(imageFile))) {
    546                 totalFileSize += Utils.copyStream(imageData, os);
    547             }
    548         }
    549     }
    550 
    551     public synchronized void cleanSmallFiles(int size) {
    552         for (ProjectionEntries projectionEntries: entries.values()) {
    553             Iterator<CacheEntry> it = projectionEntries.entries.iterator();
    554             while (it.hasNext()) {
    555                 File file = getImageFile(projectionEntries, it.next());
    556                 long length = file.length();
    557                 if (length <= size) {
    558                     if (length == 0) {
    559                         totalFileSizeDirty = true; // File probably doesn't exist
    560                     }
    561                     totalFileSize -= size;
    562                     if (!file.delete()) {
    563                         Main.warn("Unable to delete file: "+file.getAbsolutePath());
    564                     }
    565                     it.remove();
    566                 }
    567             }
    568         }
    569     }
    570 
    571     public static String printDate(Calendar c) {
    572         return DateUtils.newIsoDateFormat().format(c.getTime());
    573     }
    574 
    575     private boolean isInsideAreaToCache(CacheEntry cacheEntry) {
    576         for (ProjectionBounds b: areaToCache) {
    577             if (cacheEntry.bounds.intersects(b))
    578                 return true;
    579         }
    580         return false;
    581     }
    582 
    583     public synchronized void setAreaToCache(Set<ProjectionBounds> areaToCache) {
    584         this.areaToCache = areaToCache;
    585         Iterator<CacheEntry> it = memoryCache.keySet().iterator();
    586         while (it.hasNext()) {
    587             if (!isInsideAreaToCache(it.next())) {
    588                 it.remove();
    589             }
    590         }
    591     }
    592 }
  • src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

    diff --git a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
    index 3effadd..09b976c 100644
    a b import org.openstreetmap.gui.jmapviewer.OsmTileLoader;  
    2929import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
    3030import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    3131import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     32import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
    3233import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
    3334import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
    3435import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
    public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser {  
    6970                    continue;
    7071                }
    7172                try {
    72                     TileSource source = TMSLayer.getTileSource(info);
     73                    TileSource source = AbstractTMSTileSource.getTileSource(info);
    7374                    if (source != null) {
    7475                        sources.add(source);
    7576                    }
  • new file 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
    new file mode 100644
    index 0000000..c8b7fc1
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Color;
     7import java.awt.Component;
     8import java.awt.Font;
     9import java.awt.Graphics;
     10import java.awt.Graphics2D;
     11import java.awt.GridBagLayout;
     12import java.awt.Image;
     13import java.awt.Point;
     14import java.awt.Rectangle;
     15import java.awt.Toolkit;
     16import java.awt.event.ActionEvent;
     17import java.awt.event.MouseAdapter;
     18import java.awt.event.MouseEvent;
     19import java.awt.image.ImageObserver;
     20import java.io.File;
     21import java.io.IOException;
     22import java.text.SimpleDateFormat;
     23import java.util.ArrayList;
     24import java.util.Collections;
     25import java.util.Comparator;
     26import java.util.Date;
     27import java.util.LinkedList;
     28import java.util.List;
     29import java.util.Map;
     30import java.util.Map.Entry;
     31import java.util.Set;
     32import java.util.concurrent.ConcurrentSkipListSet;
     33
     34import javax.swing.AbstractAction;
     35import javax.swing.Action;
     36import javax.swing.BorderFactory;
     37import javax.swing.JCheckBoxMenuItem;
     38import javax.swing.JLabel;
     39import javax.swing.JMenuItem;
     40import javax.swing.JOptionPane;
     41import javax.swing.JPanel;
     42import javax.swing.JPopupMenu;
     43import javax.swing.JTextField;
     44
     45import org.openstreetmap.gui.jmapviewer.AttributionSupport;
     46import org.openstreetmap.gui.jmapviewer.Coordinate;
     47import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
     48import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
     49import org.openstreetmap.gui.jmapviewer.Tile;
     50import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     51import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     52import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     53import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     54import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     55import org.openstreetmap.josm.Main;
     56import org.openstreetmap.josm.actions.RenameLayerAction;
     57import org.openstreetmap.josm.actions.SaveActionBase;
     58import org.openstreetmap.josm.data.Bounds;
     59import org.openstreetmap.josm.data.coor.EastNorth;
     60import org.openstreetmap.josm.data.coor.LatLon;
     61import org.openstreetmap.josm.data.imagery.ImageryInfo;
     62import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     63import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     64import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     65import org.openstreetmap.josm.data.preferences.BooleanProperty;
     66import org.openstreetmap.josm.data.preferences.IntegerProperty;
     67import org.openstreetmap.josm.gui.ExtendedDialog;
     68import org.openstreetmap.josm.gui.MapFrame;
     69import org.openstreetmap.josm.gui.MapView;
     70import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
     71import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
     72import org.openstreetmap.josm.gui.PleaseWaitRunnable;
     73import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
     74import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
     75import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     76import org.openstreetmap.josm.io.WMSLayerImporter;
     77import org.openstreetmap.josm.tools.GBC;
     78
     79/**
     80 *
     81 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
     82 *
     83 * It implements all standard functions of tilesource based layers: autozoom,  tile reloads, layer saving, loading,etc.
     84 *
     85 * @author Wiktor Niesiobędzki
     86 * @since TODO
     87 *
     88 */
     89public abstract class AbstractTileSourceLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
     90    private static final String PREFERENCE_PREFIX   = "imagery.generic";
     91
     92    /** maximum zoom level supported */
     93    public static final int MAX_ZOOM = 30;
     94    /** minium zoom level supported */
     95    public static final int MIN_ZOOM = 2;
     96    private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
     97
     98    /** do set autozoom when creating a new layer */
     99    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
     100    /** do set autoload when creating a new layer */
     101    public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
     102    /** do set showerrors when creating a new layer */
     103    public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
     104    /** minimum zoom level to show to user */
     105    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
     106    /** maximum zoom level to show to user */
     107    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
     108
     109    //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
     110    /**
     111     * Zoomlevel at which tiles is currently downloaded.
     112     * Initial zoom lvl is set to bestZoom
     113     */
     114    public int currentZoomLevel;
     115    private boolean needRedraw;
     116
     117    private AttributionSupport attribution = new AttributionSupport();
     118    Tile showMetadataTile;
     119
     120    // needed public access for session exporter
     121    /** if layers changes automatically, when user zooms in */
     122    public boolean autoZoom;
     123    /** if layer automatically loads new tiles */
     124    public boolean autoLoad;
     125    /** if layer should show errors on tiles */
     126    public boolean showErrors;
     127
     128    protected TileCache tileCache;
     129    protected TileSource tileSource;
     130    protected TileLoader tileLoader;
     131
     132    /**
     133     * Creates Tile Source based Imagery Layer based on Imagery Info
     134     * @param info
     135     */
     136    public AbstractTileSourceLayer(ImageryInfo info) {
     137        super(info);
     138
     139        if(!isProjectionSupported(Main.getProjection())) {
     140            JOptionPane.showMessageDialog(Main.parent,
     141                    tr("This layer do not support the projection {0}.\n{1}\n"
     142                            + "Change the projection or remove the layer.",
     143                            Main.getProjection().toCode(), nameSupportedProjections()),
     144                            tr("Warning"),
     145                            JOptionPane.WARNING_MESSAGE);
     146        }
     147        setBackgroundLayer(true);
     148        this.setVisible(true);
     149
     150        initTileSource(getTileSource(info));
     151        MapView.addZoomChangeListener(this);
     152    }
     153
     154    protected abstract TileLoaderFactory getTileLoaderFactory();
     155
     156    /**
     157     *
     158     * @param info
     159     * @return TileSource for specified ImageryInfo
     160     * @throws IllegalArgumentException when Imagery is not supported by layer
     161     */
     162    protected abstract TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException;
     163
     164    protected abstract Map<String, String> getHeaders(TileSource tileSource);
     165
     166    protected void initTileSource(TileSource tileSource) {
     167        this.tileSource = tileSource;
     168        attribution.initialize(tileSource);
     169
     170        currentZoomLevel = getBestZoom();
     171
     172        Map<String, String> headers = getHeaders(tileSource);
     173
     174        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
     175        if (tileLoader instanceof TMSCachedTileLoader) {
     176            tileCache = (TileCache) tileLoader;
     177        } else {
     178            tileCache = new MemoryTileCache();
     179        }
     180        if (tileLoader == null)
     181            tileLoader = new OsmTileLoader(this);
     182    }
     183
     184
     185
     186    @Override
     187    public synchronized void tileLoadingFinished(Tile tile, boolean success) {
     188        if (tile.hasError()) {
     189            success = false;
     190            tile.setImage(null);
     191        }
     192        if (sharpenLevel != 0 && success) {
     193            tile.setImage(sharpenImage(tile.getImage()));
     194        }
     195        tile.setLoaded(success);
     196        needRedraw = true;
     197        if (Main.map != null) {
     198            Main.map.repaint(100);
     199        }
     200        if (Main.isDebugEnabled()) {
     201            Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
     202        }
     203    }
     204
     205    /**
     206     * Clears the tile cache.
     207     *
     208     * If the current tileLoader is an instance of OsmTileLoader, a new
     209     * TmsTileClearController is created and passed to the according clearCache
     210     * method.
     211     *
     212     * @param monitor not used in this implementation - as cache clear is instaneus
     213     */
     214    public void clearTileCache(ProgressMonitor monitor) {
     215        tileCache.clear();
     216        if (tileLoader instanceof CachedTileLoader) {
     217            ((CachedTileLoader)tileLoader).clearCache(tileSource);
     218        }
     219    }
     220
     221
     222    /**
     223     * Initiates a repaint of Main.map
     224     *
     225     * @see Main#map
     226     * @see MapFrame#repaint()
     227     */
     228    protected void redraw() {
     229        needRedraw = true;
     230        Main.map.repaint();
     231    }
     232
     233    /**
     234     * Marks layer as needing redraw on offset change
     235     */
     236    @Override
     237    public void setOffset(double dx, double dy) {
     238        super.setOffset(dx, dy);
     239        needRedraw = true;
     240    }
     241
     242
     243    /**
     244     * Returns average number of screen pixels per tile pixel for current mapview
     245     */
     246    private double getScaleFactor(int zoom) {
     247        if (!Main.isDisplayingMapView()) return 1;
     248        MapView mv = Main.map.mapView;
     249        LatLon topLeft = mv.getLatLon(0, 0);
     250        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
     251        double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
     252        double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
     253        double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
     254        double y2 = tileSource.latToTileY(botRight.lat(), zoom);
     255
     256        int screenPixels = mv.getWidth()*mv.getHeight();
     257        double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
     258        if (screenPixels == 0 || tilePixels == 0) return 1;
     259        return screenPixels/tilePixels;
     260    }
     261
     262    private final int getBestZoom() {
     263        double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
     264        double result = Math.log(factor)/Math.log(2)/2+1;
     265        /*
     266         * Math.log(factor)/Math.log(2) - gives log base 2 of factor
     267         * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
     268         * In general, smaller zoom levels are more readable.  We prefer big,
     269         * block, pixelated (but readable) map text to small, smeared,
     270         * unreadable underzoomed text.  So, use .floor() instead of rounding
     271         * to skew things a bit toward the lower zooms.
     272         * Remember, that result here, should correspond to TMSLayer.paint(...)
     273         * getScaleFactor(...) is supposed to be between 0.75 and 3
     274         */
     275        int intResult = (int)Math.floor(result);
     276        if (intResult > getMaxZoomLvl())
     277            return getMaxZoomLvl();
     278        if (intResult < getMinZoomLvl())
     279            return getMinZoomLvl();
     280        return intResult;
     281    }
     282
     283
     284    private final static boolean actionSupportLayers(List<Layer> layers) {
     285        return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
     286    }
     287
     288    private class AutoZoomAction extends AbstractAction implements LayerAction {
     289        public AutoZoomAction() {
     290            super(tr("Auto Zoom"));
     291        }
     292        @Override
     293        public void actionPerformed(ActionEvent ae) {
     294            autoZoom = !autoZoom;
     295        }
     296
     297        public Component createMenuComponent() {
     298            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     299            item.setSelected(autoZoom);
     300            return item;
     301        }
     302        @Override
     303        public boolean supportLayers(List<Layer> layers) {
     304            return actionSupportLayers(layers);
     305        }
     306
     307    }
     308
     309    private class AutoLoadTilesAction extends AbstractAction implements LayerAction {
     310        public AutoLoadTilesAction() {
     311            super(tr("Auto load tiles"));
     312        }
     313        @Override
     314        public void actionPerformed(ActionEvent ae) {
     315            autoLoad= !autoLoad;
     316        }
     317
     318        public Component createMenuComponent() {
     319            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     320            item.setSelected(autoLoad);
     321            return item;
     322        }
     323        @Override
     324        public boolean supportLayers(List<Layer> layers) {
     325            return actionSupportLayers(layers);
     326        }
     327    }
     328
     329    private class LoadAllTilesAction extends AbstractAction {
     330        public LoadAllTilesAction() {
     331            super(tr("Load All Tiles"));
     332        }
     333        @Override
     334        public void actionPerformed(ActionEvent ae) {
     335            loadAllTiles(true);
     336            redraw();
     337        }
     338    }
     339
     340    private class LoadErroneusTilesAction extends AbstractAction {
     341        public LoadErroneusTilesAction() {
     342            super(tr("Load All Error Tiles"));
     343        }
     344
     345        @Override
     346        public void actionPerformed(ActionEvent ae) {
     347            loadAllErrorTiles(true);
     348            redraw();
     349        }
     350    }
     351
     352    private class ZoomToNativeLevelAction extends AbstractAction {
     353        public ZoomToNativeLevelAction() {
     354            super(tr("Zoom to native resolution"));
     355        }
     356        @Override
     357        public void actionPerformed(ActionEvent ae) {
     358            double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
     359            Main.map.mapView.zoomToFactor(new_factor);
     360            redraw();
     361        }
     362    }
     363
     364    private class ZoomToBestAction extends AbstractAction {
     365        public ZoomToBestAction() {
     366            super(tr("Change resolution"));
     367        }
     368        @Override
     369        public void actionPerformed(ActionEvent ae) {
     370            setZoomLevel(getBestZoom());
     371        }
     372    }
     373
     374    /*
     375     * Simple class to keep clickedTile within hookUpMapView
     376     */
     377    private class TileHolder {
     378        private Tile t = null;
     379
     380        public Tile getTile() {
     381            return t;
     382        }
     383
     384        public void setTile(Tile t) {
     385            this.t = t;
     386        }
     387    }
     388
     389    /**
     390     * Creates popup menu items and binds to mouse actions
     391     */
     392    @Override
     393    public void hookUpMapView() {
     394        // keep them final here, so we avoid namespace clutter in the class
     395        final JPopupMenu tileOptionMenu = new JPopupMenu();
     396        final TileHolder clickedTileHolder = new TileHolder();
     397
     398        autoZoom = PROP_DEFAULT_AUTOZOOM.get();
     399        JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
     400        autoZoomPopup.setAction(new AutoZoomAction());
     401        autoZoomPopup.setSelected(autoZoom);
     402        tileOptionMenu.add(autoZoomPopup);
     403
     404        autoLoad = PROP_DEFAULT_AUTOLOAD.get();
     405        JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
     406        autoLoadPopup.setAction(new AutoLoadTilesAction());
     407        autoLoadPopup.setSelected(autoLoad);
     408        tileOptionMenu.add(autoLoadPopup);
     409
     410        showErrors = PROP_DEFAULT_SHOWERRORS.get();
     411        JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
     412        showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
     413            @Override
     414            public void actionPerformed(ActionEvent ae) {
     415                showErrors = !showErrors;
     416            }
     417        });
     418        showErrorsPopup.setSelected(showErrors);
     419        tileOptionMenu.add(showErrorsPopup);
     420
     421        tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
     422            @Override
     423            public void actionPerformed(ActionEvent ae) {
     424                Tile clickedTile = clickedTileHolder.getTile();
     425                if (clickedTile != null) {
     426                    loadTile(clickedTile, true);
     427                    redraw();
     428                }
     429            }
     430        }));
     431
     432        tileOptionMenu.add(new JMenuItem(new AbstractAction(
     433                tr("Show Tile Info")) {
     434            private String getSizeString(int size) {
     435                StringBuilder ret = new StringBuilder();
     436                return ret.append(size).append("x").append(size).toString();
     437            }
     438
     439            private JTextField createTextField(String text) {
     440                JTextField ret = new JTextField(text);
     441                ret.setEditable(false);
     442                ret.setBorder(BorderFactory.createEmptyBorder());
     443                return ret;
     444            }
     445
     446            @Override
     447            public void actionPerformed(ActionEvent ae) {
     448                Tile clickedTile = clickedTileHolder.getTile();
     449                if (clickedTile != null) {
     450                    ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
     451                    JPanel panel = new JPanel(new GridBagLayout());
     452                    Rectangle displaySize = tileToRect(clickedTile);
     453                    String url = "";
     454                    try {
     455                        url = clickedTile.getUrl();
     456                    } catch (IOException e) {
     457                        // silence exceptions
     458                    }
     459
     460                    String[][] content = {
     461                            {"Tile name", clickedTile.getKey()},
     462                            {"Tile url", url},
     463                            {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
     464                            {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
     465                    };
     466
     467                    for (String[] entry: content) {
     468                        panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
     469                        panel.add(GBC.glue(5,0), GBC.std());
     470                        panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
     471                    }
     472
     473                    for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
     474                        panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
     475                        panel.add(GBC.glue(5,0), GBC.std());
     476                        String value = e.getValue();
     477                        if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
     478                            value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
     479                        }
     480                        panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
     481
     482                    }
     483                    ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
     484                    ed.setContent(panel);
     485                    ed.showDialog();
     486                }
     487            }
     488        }));
     489
     490        tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction()));
     491        tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction()));
     492
     493        // increase and decrease commands
     494        tileOptionMenu.add(new JMenuItem(new AbstractAction(
     495                tr("Increase zoom")) {
     496            @Override
     497            public void actionPerformed(ActionEvent ae) {
     498                increaseZoomLevel();
     499                redraw();
     500            }
     501        }));
     502
     503        tileOptionMenu.add(new JMenuItem(new AbstractAction(
     504                tr("Decrease zoom")) {
     505            @Override
     506            public void actionPerformed(ActionEvent ae) {
     507                decreaseZoomLevel();
     508                redraw();
     509            }
     510        }));
     511
     512        tileOptionMenu.add(new JMenuItem(new AbstractAction(
     513                tr("Snap to tile size")) {
     514            @Override
     515            public void actionPerformed(ActionEvent ae) {
     516                double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
     517                Main.map.mapView.zoomToFactor(newFactor);
     518                redraw();
     519            }
     520        }));
     521
     522        tileOptionMenu.add(new JMenuItem(new AbstractAction(
     523                tr("Flush Tile Cache")) {
     524            @Override
     525            public void actionPerformed(ActionEvent ae) {
     526                new PleaseWaitRunnable(tr("Flush Tile Cache")) {
     527                    @Override
     528                    protected void realRun() {
     529                        clearTileCache(getProgressMonitor());
     530                    }
     531
     532                    @Override
     533                    protected void finish() {
     534                    }
     535
     536                    @Override
     537                    protected void cancel() {
     538                    }
     539                }.run();
     540            }
     541        }));
     542
     543        final MouseAdapter adapter = new MouseAdapter() {
     544            @Override
     545            public void mouseClicked(MouseEvent e) {
     546                if (!isVisible()) return;
     547                if (e.getButton() == MouseEvent.BUTTON3) {
     548                    clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
     549                    tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
     550                } else if (e.getButton() == MouseEvent.BUTTON1) {
     551                    attribution.handleAttribution(e.getPoint(), true);
     552                }
     553            }
     554        };
     555        Main.map.mapView.addMouseListener(adapter);
     556
     557        MapView.addLayerChangeListener(new LayerChangeListener() {
     558            @Override
     559            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
     560                //
     561            }
     562
     563            @Override
     564            public void layerAdded(Layer newLayer) {
     565                //
     566            }
     567
     568            @Override
     569            public void layerRemoved(Layer oldLayer) {
     570                if (oldLayer == AbstractTileSourceLayer.this) {
     571                    Main.map.mapView.removeMouseListener(adapter);
     572                    MapView.removeLayerChangeListener(this);
     573                    MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
     574                }
     575            }
     576        });
     577    }
     578
     579    /**
     580     * Checks zoom level against settings
     581     * @param maxZoomLvl zoom level to check
     582     * @param ts tile source to crosscheck with
     583     * @return maximum zoom level, not higher than supported by tilesource nor set by the user
     584     */
     585    public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
     586        if(maxZoomLvl > MAX_ZOOM) {
     587            maxZoomLvl = MAX_ZOOM;
     588        }
     589        if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
     590            maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
     591        }
     592        if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
     593            maxZoomLvl = ts.getMaxZoom();
     594        }
     595        return maxZoomLvl;
     596    }
     597
     598    /**
     599     * Checks zoom level against settings
     600     * @param minZoomLvl zoom level to check
     601     * @param ts tile source to crosscheck with
     602     * @return minimum zoom level, not higher than supported by tilesource nor set by the user
     603     */
     604    public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
     605        if(minZoomLvl < MIN_ZOOM) {
     606            minZoomLvl = MIN_ZOOM;
     607        }
     608        if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
     609            minZoomLvl = getMaxZoomLvl(ts);
     610        }
     611        if (ts != null && ts.getMinZoom() > minZoomLvl) {
     612            minZoomLvl = ts.getMinZoom();
     613        }
     614        return minZoomLvl;
     615    }
     616
     617
     618    /**
     619     * @param ts TileSource for which we want to know maximum zoom level
     620     * @return maximum max zoom level, that will be shown on layer
     621     */
     622    public static int getMaxZoomLvl(TileSource ts) {
     623        return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
     624    }
     625
     626    /**
     627     * @param ts TileSource for which we want to know minimum zoom level
     628     * @return minimum zoom level, that will be shown on layer
     629     */
     630    public static int getMinZoomLvl(TileSource ts) {
     631        return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
     632    }
     633
     634
     635    /**
     636     * Sets maximum zoom level, that layer will attempt show
     637     * @param maxZoomLvl
     638     */
     639    public static void setMaxZoomLvl(int maxZoomLvl) {
     640        maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
     641        PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
     642    }
     643
     644    /**
     645     * Sets minimum zoom level, that layer will attempt show
     646     * @param minZoomLvl
     647     */
     648    public static void setMinZoomLvl(int minZoomLvl) {
     649        minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
     650        PROP_MIN_ZOOM_LVL.put(minZoomLvl);
     651    }
     652
     653
     654    /**
     655     * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
     656     * changes to visible map (panning/zooming)
     657     */
     658    @Override
     659    public void zoomChanged() {
     660        if (Main.isDebugEnabled()) {
     661            Main.debug("zoomChanged(): " + currentZoomLevel);
     662        }
     663        if (tileLoader instanceof TMSCachedTileLoader) {
     664            ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
     665        }
     666        needRedraw = true;
     667    }
     668
     669    protected int getMaxZoomLvl() {
     670        if (info.getMaxZoom() != 0)
     671            return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
     672        else
     673            return getMaxZoomLvl(tileSource);
     674    }
     675
     676    protected int getMinZoomLvl() {
     677        return getMinZoomLvl(tileSource);
     678    }
     679
     680    /**
     681     *
     682     * @return if its allowed to zoom in
     683     */
     684    public boolean zoomIncreaseAllowed() {
     685        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
     686        if (Main.isDebugEnabled()) {
     687            Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
     688        }
     689        return zia;
     690    }
     691
     692    /**
     693     * Zoom in, go closer to map.
     694     *
     695     * @return    true, if zoom increasing was successful, false otherwise
     696     */
     697    public boolean increaseZoomLevel() {
     698        if (zoomIncreaseAllowed()) {
     699            currentZoomLevel++;
     700            if (Main.isDebugEnabled()) {
     701                Main.debug("increasing zoom level to: " + currentZoomLevel);
     702            }
     703            zoomChanged();
     704        } else {
     705            Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
     706                    "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
     707            return false;
     708        }
     709        return true;
     710    }
     711
     712    /**
     713     * Sets the zoom level of the layer
     714     * @param zoom zoom level
     715     * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
     716     */
     717    public boolean setZoomLevel(int zoom) {
     718        if (zoom == currentZoomLevel) return true;
     719        if (zoom > this.getMaxZoomLvl()) return false;
     720        if (zoom < this.getMinZoomLvl()) return false;
     721        currentZoomLevel = zoom;
     722        zoomChanged();
     723        return true;
     724    }
     725
     726    /**
     727     * Check if zooming out is allowed
     728     *
     729     * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
     730     */
     731    public boolean zoomDecreaseAllowed() {
     732        return currentZoomLevel > this.getMinZoomLvl();
     733    }
     734
     735    /**
     736     * Zoom out from map.
     737     *
     738     * @return    true, if zoom increasing was successfull, false othervise
     739     */
     740    public boolean decreaseZoomLevel() {
     741        //int minZoom = this.getMinZoomLvl();
     742        if (zoomDecreaseAllowed()) {
     743            if (Main.isDebugEnabled()) {
     744                Main.debug("decreasing zoom level to: " + currentZoomLevel);
     745            }
     746            currentZoomLevel--;
     747            zoomChanged();
     748        } else {
     749            /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
     750            return false;
     751        }
     752        return true;
     753    }
     754
     755    /*
     756     * We use these for quick, hackish calculations.  They
     757     * are temporary only and intentionally not inserted
     758     * into the tileCache.
     759     */
     760    private Tile tempCornerTile(Tile t) {
     761        int x = t.getXtile() + 1;
     762        int y = t.getYtile() + 1;
     763        int zoom = t.getZoom();
     764        Tile tile = getTile(x, y, zoom);
     765        if (tile != null)
     766            return tile;
     767        return new Tile(tileSource, x, y, zoom);
     768    }
     769
     770    private Tile getOrCreateTile(int x, int y, int zoom) {
     771        Tile tile = getTile(x, y, zoom);
     772        if (tile == null) {
     773            tile = new Tile(tileSource, x, y, zoom);
     774            tileCache.addTile(tile);
     775            tile.loadPlaceholderFromCache(tileCache);
     776        }
     777        return tile;
     778    }
     779
     780    /*
     781     * This can and will return null for tiles that are not
     782     * already in the cache.
     783     */
     784    private Tile getTile(int x, int y, int zoom) {
     785        int max = (1 << zoom);
     786        if (x < 0 || x >= max || y < 0 || y >= max)
     787            return null;
     788        return tileCache.getTile(tileSource, x, y, zoom);
     789    }
     790
     791    private boolean loadTile(Tile tile, boolean force) {
     792        if (tile == null)
     793            return false;
     794        if (!force && (tile.isLoaded() || tile.hasError()))
     795            return false;
     796        if (tile.isLoading())
     797            return false;
     798        tileLoader.createTileLoaderJob(tile).submit();
     799        return true;
     800    }
     801
     802    private TileSet getVisibleTileSet() {
     803        MapView mv = Main.map.mapView;
     804        EastNorth topLeft = mv.getEastNorth(0, 0);
     805        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
     806        return new TileSet(topLeft, botRight, currentZoomLevel);
     807    }
     808
     809    private void loadAllTiles(boolean force) {
     810        TileSet ts = getVisibleTileSet();
     811
     812        // if there is more than 18 tiles on screen in any direction, do not
     813        // load all tiles!
     814        if (ts.tooLarge()) {
     815            Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
     816            return;
     817        }
     818        ts.loadAllTiles(force);
     819    }
     820
     821    private void loadAllErrorTiles(boolean force) {
     822        TileSet ts = getVisibleTileSet();
     823        ts.loadAllErrorTiles(force);
     824    }
     825
     826    @Override
     827    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
     828        boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
     829        needRedraw = true;
     830        if (Main.isDebugEnabled()) {
     831            Main.debug("imageUpdate() done: " + done + " calling repaint");
     832        }
     833        Main.map.repaint(done ? 0 : 100);
     834        return !done;
     835    }
     836
     837    private boolean imageLoaded(Image i) {
     838        if (i == null)
     839            return false;
     840        int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
     841        if ((status & ALLBITS) != 0)
     842            return true;
     843        return false;
     844    }
     845
     846    /**
     847     * Returns the image for the given tile if both tile and image are loaded.
     848     * Otherwise returns  null.
     849     *
     850     * @param tile the Tile for which the image should be returned
     851     * @return  the image of the tile or null.
     852     */
     853    private Image getLoadedTileImage(Tile tile) {
     854        if (!tile.isLoaded())
     855            return null;
     856        Image img = tile.getImage();
     857        if (!imageLoaded(img))
     858            return null;
     859        return img;
     860    }
     861
     862    private LatLon tileLatLon(Tile t) {
     863        int zoom = t.getZoom();
     864        return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
     865                tileSource.tileXToLon(t.getXtile(), zoom));
     866    }
     867
     868    private Rectangle tileToRect(Tile t1) {
     869        /*
     870         * We need to get a box in which to draw, so advance by one tile in
     871         * each direction to find the other corner of the box.
     872         * Note: this somewhat pollutes the tile cache
     873         */
     874        Tile t2 = tempCornerTile(t1);
     875        Rectangle rect = new Rectangle(pixelPos(t1));
     876        rect.add(pixelPos(t2));
     877        return rect;
     878    }
     879
     880    // 'source' is the pixel coordinates for the area that
     881    // the img is capable of filling in.  However, we probably
     882    // only want a portion of it.
     883    //
     884    // 'border' is the screen cordinates that need to be drawn.
     885    //  We must not draw outside of it.
     886    private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
     887        Rectangle target = source;
     888
     889        // If a border is specified, only draw the intersection
     890        // if what we have combined with what we are supposed
     891        // to draw.
     892        if (border != null) {
     893            target = source.intersection(border);
     894            if (Main.isDebugEnabled()) {
     895                Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
     896            }
     897        }
     898
     899        // All of the rectangles are in screen coordinates.  We need
     900        // to how these correlate to the sourceImg pixels.  We could
     901        // avoid doing this by scaling the image up to the 'source' size,
     902        // but this should be cheaper.
     903        //
     904        // In some projections, x any y are scaled differently enough to
     905        // cause a pixel or two of fudge.  Calculate them separately.
     906        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
     907        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
     908
     909        // How many pixels into the 'source' rectangle are we drawing?
     910        int screen_x_offset = target.x - source.x;
     911        int screen_y_offset = target.y - source.y;
     912        // And how many pixels into the image itself does that
     913        // correlate to?
     914        int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
     915        int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
     916        // Now calculate the other corner of the image that we need
     917        // by scaling the 'target' rectangle's dimensions.
     918        int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
     919        int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
     920
     921        if (Main.isDebugEnabled()) {
     922            Main.debug("drawing image into target rect: " + target);
     923        }
     924        g.drawImage(sourceImg,
     925                target.x, target.y,
     926                target.x + target.width, target.y + target.height,
     927                img_x_offset, img_y_offset,
     928                img_x_end, img_y_end,
     929                this);
     930        if (PROP_FADE_AMOUNT.get() != 0) {
     931            // dimm by painting opaque rect...
     932            g.setColor(getFadeColorWithAlpha());
     933            g.fillRect(target.x, target.y,
     934                    target.width, target.height);
     935        }
     936    }
     937
     938    // This function is called for several zoom levels, not just
     939    // the current one.  It should not trigger any tiles to be
     940    // downloaded.  It should also avoid polluting the tile cache
     941    // with any tiles since these tiles are not mandatory.
     942    //
     943    // The "border" tile tells us the boundaries of where we may
     944    // draw.  It will not be from the zoom level that is being
     945    // drawn currently.  If drawing the displayZoomLevel,
     946    // border is null and we draw the entire tile set.
     947    private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
     948        if (zoom <= 0) return Collections.emptyList();
     949        Rectangle borderRect = null;
     950        if (border != null) {
     951            borderRect = tileToRect(border);
     952        }
     953        List<Tile> missedTiles = new LinkedList<>();
     954        // The callers of this code *require* that we return any tiles
     955        // that we do not draw in missedTiles.  ts.allExistingTiles() by
     956        // default will only return already-existing tiles.  However, we
     957        // need to return *all* tiles to the callers, so force creation
     958        // here.
     959        //boolean forceTileCreation = true;
     960        for (Tile tile : ts.allTilesCreate()) {
     961            Image img = getLoadedTileImage(tile);
     962            if (img == null || tile.hasError()) {
     963                if (Main.isDebugEnabled()) {
     964                    Main.debug("missed tile: " + tile);
     965                }
     966                missedTiles.add(tile);
     967                continue;
     968            }
     969            Rectangle sourceRect = tileToRect(tile);
     970            if (borderRect != null && !sourceRect.intersects(borderRect)) {
     971                continue;
     972            }
     973            drawImageInside(g, img, sourceRect, borderRect);
     974        }
     975        return missedTiles;
     976    }
     977
     978    private void myDrawString(Graphics g, String text, int x, int y) {
     979        Color oldColor = g.getColor();
     980        g.setColor(Color.black);
     981        g.drawString(text,x+1,y+1);
     982        g.setColor(oldColor);
     983        g.drawString(text,x,y);
     984    }
     985
     986    private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
     987        int fontHeight = g.getFontMetrics().getHeight();
     988        if (tile == null)
     989            return;
     990        Point p = pixelPos(t);
     991        int texty = p.y + 2 + fontHeight;
     992
     993        /*if (PROP_DRAW_DEBUG.get()) {
     994            myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
     995            texty += 1 + fontHeight;
     996            if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
     997                myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
     998                texty += 1 + fontHeight;
     999            }
     1000        }*/
     1001
     1002        /*String tileStatus = tile.getStatus();
     1003        if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
     1004            myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
     1005            texty += 1 + fontHeight;
     1006        }*/
     1007
     1008        if (tile.hasError() && showErrors) {
     1009            myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
     1010            texty += 1 + fontHeight;
     1011        }
     1012
     1013        /*int xCursor = -1;
     1014        int yCursor = -1;
     1015        if (PROP_DRAW_DEBUG.get()) {
     1016            if (yCursor < t.getYtile()) {
     1017                if (t.getYtile() % 32 == 31) {
     1018                    g.fillRect(0, p.y - 1, mv.getWidth(), 3);
     1019                } else {
     1020                    g.drawLine(0, p.y, mv.getWidth(), p.y);
     1021                }
     1022                yCursor = t.getYtile();
     1023            }
     1024            // This draws the vertical lines for the entire
     1025            // column. Only draw them for the top tile in
     1026            // the column.
     1027            if (xCursor < t.getXtile()) {
     1028                if (t.getXtile() % 32 == 0) {
     1029                    // level 7 tile boundary
     1030                    g.fillRect(p.x - 1, 0, 3, mv.getHeight());
     1031                } else {
     1032                    g.drawLine(p.x, 0, p.x, mv.getHeight());
     1033                }
     1034                xCursor = t.getXtile();
     1035            }
     1036        }*/
     1037    }
     1038
     1039    private Point pixelPos(LatLon ll) {
     1040        return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
     1041    }
     1042
     1043    private Point pixelPos(Tile t) {
     1044        double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
     1045        LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
     1046        return pixelPos(tmpLL);
     1047    }
     1048
     1049    private LatLon getShiftedLatLon(EastNorth en) {
     1050        return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
     1051    }
     1052
     1053    private Coordinate getShiftedCoord(EastNorth en) {
     1054        LatLon ll = getShiftedLatLon(en);
     1055        return new Coordinate(ll.lat(),ll.lon());
     1056    }
     1057
     1058    private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
     1059    private final class TileSet {
     1060        int x0, x1, y0, y1;
     1061        int zoom;
     1062        int tileMax = -1;
     1063
     1064        /**
     1065         * Create a TileSet by EastNorth bbox taking a layer shift in account
     1066         */
     1067        private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
     1068            this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
     1069        }
     1070
     1071        /**
     1072         * Create a TileSet by known LatLon bbox without layer shift correction
     1073         */
     1074        private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
     1075            this.zoom = zoom;
     1076            if (zoom == 0)
     1077                return;
     1078
     1079            x0 = (int)tileSource.lonToTileX(topLeft.lon(),  zoom);
     1080            y0 = (int)tileSource.latToTileY(topLeft.lat(),  zoom);
     1081            x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
     1082            y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
     1083            if (x0 > x1) {
     1084                int tmp = x0;
     1085                x0 = x1;
     1086                x1 = tmp;
     1087            }
     1088            if (y0 > y1) {
     1089                int tmp = y0;
     1090                y0 = y1;
     1091                y1 = tmp;
     1092            }
     1093            tileMax = (int)Math.pow(2.0, zoom);
     1094            if (x0 < 0) {
     1095                x0 = 0;
     1096            }
     1097            if (y0 < 0) {
     1098                y0 = 0;
     1099            }
     1100            if (x1 > tileMax) {
     1101                x1 = tileMax;
     1102            }
     1103            if (y1 > tileMax) {
     1104                y1 = tileMax;
     1105            }
     1106        }
     1107
     1108        private boolean tooSmall() {
     1109            return this.tilesSpanned() < 2.1;
     1110        }
     1111
     1112        private  boolean tooLarge() {
     1113            return this.tilesSpanned() > 10;
     1114        }
     1115
     1116        private boolean insane() {
     1117            return this.tilesSpanned() > 100;
     1118        }
     1119
     1120        private double tilesSpanned() {
     1121            return Math.sqrt(1.0 * this.size());
     1122        }
     1123
     1124        private int size() {
     1125            int x_span = x1 - x0 + 1;
     1126            int y_span = y1 - y0 + 1;
     1127            return x_span * y_span;
     1128        }
     1129
     1130        /*
     1131         * Get all tiles represented by this TileSet that are
     1132         * already in the tileCache.
     1133         */
     1134        private List<Tile> allExistingTiles() {
     1135            return this.__allTiles(false);
     1136        }
     1137
     1138        private List<Tile> allTilesCreate() {
     1139            return this.__allTiles(true);
     1140        }
     1141
     1142        private List<Tile> __allTiles(boolean create) {
     1143            // Tileset is either empty or too large
     1144            if (zoom == 0 || this.insane())
     1145                return Collections.emptyList();
     1146            List<Tile> ret = new ArrayList<>();
     1147            for (int x = x0; x <= x1; x++) {
     1148                for (int y = y0; y <= y1; y++) {
     1149                    Tile t;
     1150                    if (create) {
     1151                        t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
     1152                    } else {
     1153                        t = getTile(x % tileMax, y % tileMax, zoom);
     1154                    }
     1155                    if (t != null) {
     1156                        ret.add(t);
     1157                    }
     1158                }
     1159            }
     1160            return ret;
     1161        }
     1162
     1163        private List<Tile> allLoadedTiles() {
     1164            List<Tile> ret = new ArrayList<>();
     1165            for (Tile t : this.allExistingTiles()) {
     1166                if (t.isLoaded())
     1167                    ret.add(t);
     1168            }
     1169            return ret;
     1170        }
     1171
     1172        /**
     1173         * @return comparator, that sorts the tiles from the center to the edge of the current screen
     1174         */
     1175        private Comparator<Tile> getTileDistanceComparator() {
     1176            final int centerX = (int) Math.ceil((x0 + x1) / 2d);
     1177            final int centerY = (int) Math.ceil((y0 + y1) / 2d);
     1178            return new Comparator<Tile>() {
     1179                private int getDistance(Tile t) {
     1180                    return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
     1181                }
     1182                @Override
     1183                public int compare(Tile o1, Tile o2) {
     1184                    int distance1 = getDistance(o1);
     1185                    int distance2 = getDistance(o2);
     1186                    return Integer.compare(distance1, distance2);
     1187                }
     1188            };
     1189        }
     1190
     1191
     1192        private void loadAllTiles(boolean force) {
     1193            if (!autoLoad && !force)
     1194                return;
     1195            List<Tile> allTiles = allTilesCreate();
     1196            Collections.sort(allTiles, getTileDistanceComparator());
     1197            for (Tile t : allTiles) {
     1198                loadTile(t, false);
     1199            }
     1200        }
     1201
     1202        private void loadAllErrorTiles(boolean force) {
     1203            if (!autoLoad && !force)
     1204                return;
     1205            for (Tile t : this.allTilesCreate()) {
     1206                if (t.hasError()) {
     1207                    loadTile(t, true);
     1208                }
     1209            }
     1210        }
     1211    }
     1212
     1213
     1214    private static class TileSetInfo {
     1215        public boolean hasVisibleTiles = false;
     1216        public boolean hasOverzoomedTiles = false;
     1217        public boolean hasLoadingTiles = false;
     1218    }
     1219
     1220    private static TileSetInfo getTileSetInfo(TileSet ts) {
     1221        List<Tile> allTiles = ts.allExistingTiles();
     1222        TileSetInfo result = new TileSetInfo();
     1223        result.hasLoadingTiles = allTiles.size() < ts.size();
     1224        for (Tile t : allTiles) {
     1225            if (t.isLoaded()) {
     1226                if (!t.hasError()) {
     1227                    result.hasVisibleTiles = true;
     1228                }
     1229                if ("no-tile".equals(t.getValue("tile-info"))) {
     1230                    result.hasOverzoomedTiles = true;
     1231                }
     1232            } else {
     1233                result.hasLoadingTiles = true;
     1234            }
     1235        }
     1236        return result;
     1237    }
     1238
     1239    private class DeepTileSet {
     1240        private final EastNorth topLeft, botRight;
     1241        private final int minZoom, maxZoom;
     1242        private final TileSet[] tileSets;
     1243        private final TileSetInfo[] tileSetInfos;
     1244        public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
     1245            this.topLeft = topLeft;
     1246            this.botRight = botRight;
     1247            this.minZoom = minZoom;
     1248            this.maxZoom = maxZoom;
     1249            this.tileSets = new TileSet[maxZoom - minZoom + 1];
     1250            this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
     1251        }
     1252        public TileSet getTileSet(int zoom) {
     1253            if (zoom < minZoom)
     1254                return nullTileSet;
     1255            synchronized (tileSets) {
     1256                TileSet ts = tileSets[zoom-minZoom];
     1257                if (ts == null) {
     1258                    ts = new TileSet(topLeft, botRight, zoom);
     1259                    tileSets[zoom-minZoom] = ts;
     1260                }
     1261                return ts;
     1262            }
     1263        }
     1264
     1265        public TileSetInfo getTileSetInfo(int zoom) {
     1266            if (zoom < minZoom)
     1267                return new TileSetInfo();
     1268            synchronized (tileSetInfos) {
     1269                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
     1270                if (tsi == null) {
     1271                    tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
     1272                    tileSetInfos[zoom-minZoom] = tsi;
     1273                }
     1274                return tsi;
     1275            }
     1276        }
     1277    }
     1278
     1279    @Override
     1280    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
     1281        EastNorth topLeft = mv.getEastNorth(0, 0);
     1282        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
     1283
     1284        if (botRight.east() == 0.0 || botRight.north() == 0) {
     1285            /*Main.debug("still initializing??");*/
     1286            // probably still initializing
     1287            return;
     1288        }
     1289
     1290        needRedraw = false;
     1291
     1292        int zoom = currentZoomLevel;
     1293        if (autoZoom) {
     1294            double pixelScaling = getScaleFactor(zoom);
     1295            if (pixelScaling > 3 || pixelScaling < 0.7) {
     1296                zoom = getBestZoom();
     1297            }
     1298        }
     1299
     1300        DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
     1301        TileSet ts = dts.getTileSet(zoom);
     1302
     1303        int displayZoomLevel = zoom;
     1304
     1305        boolean noTilesAtZoom = false;
     1306        if (autoZoom && autoLoad) {
     1307            // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
     1308            TileSetInfo tsi = dts.getTileSetInfo(zoom);
     1309            if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
     1310                noTilesAtZoom = true;
     1311            }
     1312            // Find highest zoom level with at least one visible tile
     1313            for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
     1314                if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
     1315                    displayZoomLevel = tmpZoom;
     1316                    break;
     1317                }
     1318            }
     1319            // Do binary search between currentZoomLevel and displayZoomLevel
     1320            while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
     1321                zoom = (zoom + displayZoomLevel)/2;
     1322                tsi = dts.getTileSetInfo(zoom);
     1323            }
     1324
     1325            setZoomLevel(zoom);
     1326
     1327            // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
     1328            // to make sure there're really no more zoom levels
     1329            if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
     1330                zoom++;
     1331                tsi = dts.getTileSetInfo(zoom);
     1332            }
     1333            // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
     1334            // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
     1335            while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
     1336                zoom--;
     1337                tsi = dts.getTileSetInfo(zoom);
     1338            }
     1339            ts = dts.getTileSet(zoom);
     1340        } else if (autoZoom) {
     1341            setZoomLevel(zoom);
     1342        }
     1343
     1344        // Too many tiles... refuse to download
     1345        if (!ts.tooLarge()) {
     1346            //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
     1347            ts.loadAllTiles(false);
     1348        }
     1349
     1350        if (displayZoomLevel != zoom) {
     1351            ts = dts.getTileSet(displayZoomLevel);
     1352        }
     1353
     1354        g.setColor(Color.DARK_GRAY);
     1355
     1356        List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
     1357        int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};
     1358        for (int zoomOffset : otherZooms) {
     1359            if (!autoZoom) {
     1360                break;
     1361            }
     1362            int newzoom = displayZoomLevel + zoomOffset;
     1363            if (newzoom < MIN_ZOOM) {
     1364                continue;
     1365            }
     1366            if (missedTiles.isEmpty()) {
     1367                break;
     1368            }
     1369            List<Tile> newlyMissedTiles = new LinkedList<>();
     1370            for (Tile missed : missedTiles) {
     1371                if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
     1372                    // Don't try to paint from higher zoom levels when tile is overzoomed
     1373                    newlyMissedTiles.add(missed);
     1374                    continue;
     1375                }
     1376                Tile t2 = tempCornerTile(missed);
     1377                LatLon topLeft2  = tileLatLon(missed);
     1378                LatLon botRight2 = tileLatLon(t2);
     1379                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
     1380                // Instantiating large TileSets is expensive.  If there
     1381                // are no loaded tiles, don't bother even trying.
     1382                if (ts2.allLoadedTiles().isEmpty()) {
     1383                    newlyMissedTiles.add(missed);
     1384                    continue;
     1385                }
     1386                if (ts2.tooLarge()) {
     1387                    continue;
     1388                }
     1389                newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
     1390            }
     1391            missedTiles = newlyMissedTiles;
     1392        }
     1393        if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
     1394            Main.debug("still missed "+missedTiles.size()+" in the end");
     1395        }
     1396        g.setColor(Color.red);
     1397        g.setFont(InfoFont);
     1398
     1399        // The current zoom tileset should have all of its tiles
     1400        // due to the loadAllTiles(), unless it to tooLarge()
     1401        for (Tile t : ts.allExistingTiles()) {
     1402            this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
     1403        }
     1404
     1405        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
     1406
     1407        //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
     1408        g.setColor(Color.lightGray);
     1409        if (!autoZoom) {
     1410            if (ts.insane()) {
     1411                myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
     1412            } else if (ts.tooLarge()) {
     1413                myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
     1414            } else if (ts.tooSmall()) {
     1415                myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
     1416            }
     1417        }
     1418        if (noTilesAtZoom) {
     1419            myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
     1420        }
     1421        if (Main.isDebugEnabled()) {
     1422            myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
     1423            myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
     1424            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
     1425            myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
     1426            if(tileLoader instanceof TMSCachedTileLoader) {
     1427                TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
     1428                int offset = 185;
     1429                for(String part: cachedTileLoader.getStats().split("\n")) {
     1430                    myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
     1431                }
     1432
     1433            }
     1434        }
     1435    }
     1436
     1437    /**
     1438     * This isn't very efficient, but it is only used when the
     1439     * user right-clicks on the map.
     1440     */
     1441    private Tile getTileForPixelpos(int px, int py) {
     1442        if (Main.isDebugEnabled()) {
     1443            Main.debug("getTileForPixelpos("+px+", "+py+")");
     1444        }
     1445        MapView mv = Main.map.mapView;
     1446        Point clicked = new Point(px, py);
     1447        EastNorth topLeft = mv.getEastNorth(0, 0);
     1448        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
     1449        int z = currentZoomLevel;
     1450        TileSet ts = new TileSet(topLeft, botRight, z);
     1451
     1452        if (!ts.tooLarge()) {
     1453            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
     1454        }
     1455        Tile clickedTile = null;
     1456        for (Tile t1 : ts.allExistingTiles()) {
     1457            Tile t2 = tempCornerTile(t1);
     1458            Rectangle r = new Rectangle(pixelPos(t1));
     1459            r.add(pixelPos(t2));
     1460            if (Main.isDebugEnabled()) {
     1461                Main.debug("r: " + r + " clicked: " + clicked);
     1462            }
     1463            if (!r.contains(clicked)) {
     1464                continue;
     1465            }
     1466            clickedTile  = t1;
     1467            break;
     1468        }
     1469        if (clickedTile == null)
     1470            return null;
     1471        /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
     1472                " currentZoomLevel: " + currentZoomLevel);*/
     1473        return clickedTile;
     1474    }
     1475
     1476    @Override
     1477    public Action[] getMenuEntries() {
     1478        return new Action[] {
     1479                LayerListDialog.getInstance().createActivateLayerAction(this),
     1480                LayerListDialog.getInstance().createShowHideLayerAction(),
     1481                LayerListDialog.getInstance().createDeleteLayerAction(),
     1482                SeparatorLayerAction.INSTANCE,
     1483                // color,
     1484                new OffsetAction(),
     1485                new RenameLayerAction(this.getAssociatedFile(), this),
     1486                SeparatorLayerAction.INSTANCE,
     1487                new AutoLoadTilesAction(),
     1488                new AutoZoomAction(),
     1489                new ZoomToBestAction(),
     1490                new ZoomToNativeLevelAction(),
     1491                new LoadErroneusTilesAction(),
     1492                new LoadAllTilesAction(),
     1493                new LayerListPopup.InfoAction(this)
     1494        };
     1495    }
     1496
     1497    @Override
     1498    public String getToolTipText() {
     1499        if(autoLoad) {
     1500            return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     1501        } else {
     1502            return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     1503        }
     1504    }
     1505
     1506    @Override
     1507    public void visitBoundingBox(BoundingXYVisitor v) {
     1508    }
     1509
     1510    @Override
     1511    public boolean isChanged() {
     1512        return needRedraw;
     1513    }
     1514
     1515    /**
     1516     * Task responsible for precaching imagery along the gpx track
     1517     *
     1518     */
     1519    public class PrecacheTask implements TileLoaderListener {
     1520        private final ProgressMonitor progressMonitor;
     1521        private volatile int totalCount;
     1522        private volatile int processedCount = 0;
     1523        private TileLoader tileLoader;
     1524
     1525        /**
     1526         * @param progressMonitor that will be notified about progess of the task
     1527         */
     1528        public PrecacheTask(ProgressMonitor progressMonitor) {
     1529            this.progressMonitor = progressMonitor;
     1530            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
     1531            if (this.tileLoader instanceof TMSCachedTileLoader) {
     1532                ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
     1533                        TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
     1534            }
     1535
     1536        }
     1537
     1538        /**
     1539         * @return true, if all is done
     1540         */
     1541        public boolean isFinished() {
     1542            return processedCount >= totalCount;
     1543        }
     1544
     1545        /**
     1546         * @return total number of tiles to download
     1547         */
     1548        public int getTotalCount() {
     1549            return totalCount;
     1550        }
     1551
     1552        /**
     1553         * cancel the task
     1554         */
     1555        public void cancel() {
     1556            if (tileLoader instanceof TMSCachedTileLoader) {
     1557                ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
     1558            }
     1559        }
     1560
     1561
     1562        @Override
     1563        public void tileLoadingFinished(Tile tile, boolean success) {
     1564            if (success) {
     1565                this.processedCount++;
     1566                this.progressMonitor.worked(1);
     1567                this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processedCount, totalCount));
     1568            }
     1569        }
     1570
     1571        /**
     1572         * @return tile loader that is used to load the tiles
     1573         */
     1574        public TileLoader getTileLoader() {
     1575            return tileLoader;
     1576        }
     1577    }
     1578
     1579    /**
     1580     * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
     1581     * all of the tiles. Buffer contains at least one tile.
     1582     *
     1583     * To prevent accidental clear of the queue, new download executor is created with separate queue
     1584     *
     1585     * @param precacheTask
     1586     * @param points
     1587     * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
     1588     * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
     1589     */
     1590    public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
     1591        final Set<Tile> requestedTiles = new ConcurrentSkipListSet <>(new Comparator<Tile>() {
     1592            public int compare(Tile o1, Tile o2) {
     1593                return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
     1594            }
     1595        });
     1596        for (LatLon point: points) {
     1597            int minY = (int) Math.round(tileSource.latToTileY(point.lat() - bufferY, currentZoomLevel));
     1598            int curY = (int) Math.round(tileSource.latToTileY(point.lat(), currentZoomLevel));
     1599            int maxY = (int) Math.round(tileSource.latToTileY(point.lat() + bufferY, currentZoomLevel));
     1600            int minX = (int) Math.round(tileSource.lonToTileX(point.lon() - bufferX, currentZoomLevel));
     1601            int curX = (int) Math.round(tileSource.lonToTileX(point.lon(), currentZoomLevel));
     1602            int maxX = (int) Math.round(tileSource.lonToTileX(point.lon() + bufferX, currentZoomLevel));
     1603
     1604            // take at least one tile of buffer
     1605            minY = Math.min(curY - 1, minY);
     1606            maxY = Math.max(curY + 1, maxY);
     1607            minX = Math.min(curX - 1, minX);
     1608            maxX = Math.min(curX + 1, maxX);
     1609
     1610            for (int x= minX; x<=maxX; x++) {
     1611                for (int y= minY; y<=maxY; y++) {
     1612                    requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1613                }
     1614            }
     1615        }
     1616
     1617        precacheTask.totalCount = requestedTiles.size();
     1618        precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
     1619
     1620        TileLoader loader = precacheTask.getTileLoader();
     1621        for (Tile t: requestedTiles) {
     1622            loader.createTileLoaderJob(t).submit();
     1623        }
     1624    }
     1625
     1626    @Override
     1627    public boolean isSavable() {
     1628        return true; // With WMSLayerExporter
     1629    }
     1630
     1631    @Override
     1632    public File createAndOpenSaveFileChooser() {
     1633        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
     1634    }
     1635}
  • 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 9201712..a539567 100644
    a b package org.openstreetmap.josm.gui.layer;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.Color;
    7 import java.awt.Font;
    8 import java.awt.Graphics;
    9 import java.awt.Graphics2D;
    10 import java.awt.GridBagLayout;
    11 import java.awt.Image;
    12 import java.awt.Point;
    13 import java.awt.Rectangle;
    14 import java.awt.Toolkit;
    15 import java.awt.event.ActionEvent;
    16 import java.awt.event.MouseAdapter;
    17 import java.awt.event.MouseEvent;
    18 import java.awt.image.ImageObserver;
    19 import java.io.File;
    206import java.io.IOException;
    21 import java.io.StringReader;
    22 import java.net.URL;
    23 import java.text.SimpleDateFormat;
    24 import java.util.ArrayList;
    25 import java.util.Collections;
    26 import java.util.Comparator;
    27 import java.util.Date;
    28 import java.util.HashMap;
    29 import java.util.LinkedList;
    30 import java.util.List;
    317import java.util.Map;
    32 import java.util.Map.Entry;
    33 import java.util.Scanner;
    34 import java.util.concurrent.Callable;
    35 import java.util.regex.Matcher;
    36 import java.util.regex.Pattern;
    378
    38 import javax.swing.AbstractAction;
    39 import javax.swing.Action;
    40 import javax.swing.BorderFactory;
    41 import javax.swing.JCheckBoxMenuItem;
    42 import javax.swing.JLabel;
    43 import javax.swing.JMenuItem;
    44 import javax.swing.JOptionPane;
    45 import javax.swing.JPanel;
    46 import javax.swing.JPopupMenu;
    47 import javax.swing.JTextField;
    48 
    49 import org.openstreetmap.gui.jmapviewer.AttributionSupport;
    50 import org.openstreetmap.gui.jmapviewer.Coordinate;
    51 import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    52 import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    53 import org.openstreetmap.gui.jmapviewer.Tile;
    54 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
    55 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
    569import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    5710import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    5811import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    59 import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
    60 import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
    61 import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
     12import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
    6213import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
    63 import org.openstreetmap.josm.Main;
    64 import org.openstreetmap.josm.actions.RenameLayerAction;
    65 import org.openstreetmap.josm.data.Bounds;
    66 import org.openstreetmap.josm.data.Version;
    67 import org.openstreetmap.josm.data.coor.EastNorth;
    68 import org.openstreetmap.josm.data.coor.LatLon;
     14import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
    6915import org.openstreetmap.josm.data.imagery.ImageryInfo;
    7016import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    7117import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    72 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     18import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
    7319import org.openstreetmap.josm.data.preferences.BooleanProperty;
    7420import org.openstreetmap.josm.data.preferences.IntegerProperty;
    75 import org.openstreetmap.josm.data.preferences.StringProperty;
    7621import org.openstreetmap.josm.data.projection.Projection;
    77 import org.openstreetmap.josm.gui.ExtendedDialog;
    78 import org.openstreetmap.josm.gui.MapFrame;
    79 import org.openstreetmap.josm.gui.MapView;
    80 import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
    81 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
    82 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
    83 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    84 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    85 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    86 import org.openstreetmap.josm.io.CacheCustomContent;
    87 import org.openstreetmap.josm.io.OsmTransferException;
    88 import org.openstreetmap.josm.io.UTFInputStreamReader;
    89 import org.openstreetmap.josm.tools.CheckParameterUtil;
    90 import org.openstreetmap.josm.tools.GBC;
    91 import org.openstreetmap.josm.tools.Utils;
    92 import org.xml.sax.InputSource;
    93 import org.xml.sax.SAXException;
     22
    9423
    9524/**
    9625 * Class that displays a slippy map layer.
    import org.xml.sax.SAXException;  
    10130 * @author Upliner &lt;upliner@gmail.com&gt;
    10231 *
    10332 */
    104 public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
    105     public static final String PREFERENCE_PREFIX   = "imagery.tms";
    106 
    107     public static final int MAX_ZOOM = 30;
    108     public static final int MIN_ZOOM = 2;
    109     public static final int DEFAULT_MAX_ZOOM = 20;
    110     public static final int DEFAULT_MIN_ZOOM = 2;
    111 
    112     public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
    113     public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
    114     public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
    115     public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
    116     public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
    117     public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX +
    118             ".add_to_slippymap_chooser", true);
    119     public static final StringProperty PROP_TILECACHE_DIR;
     33public class TMSLayer extends AbstractTileSourceLayer {
     34    private static final String PREFERENCE_PREFIX   = "imagery.tms";
    12035
    121     static {
    122         String defPath = null;
    123         try {
    124             defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
    125         } catch (SecurityException e) {
    126             Main.warn(e);
    127         }
    128         PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
    129     }
     36    /** minimum zoom level for TMS layer */
     37    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", AbstractTileSourceLayer.PROP_MIN_ZOOM_LVL.get());
     38    /** maximum zoom level for TMS layer */
     39    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", AbstractTileSourceLayer.PROP_MAX_ZOOM_LVL.get());
     40    /** shall TMS layers be added to download dialog */
     41    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
    13042
    131     /**
    132      * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map
    133      *
    134      */
    135     public interface TileLoaderFactory {
    136         /**
    137          * @param listener object that will be notified, when tile has finished loading
    138          * @return TileLoader that will notify the listener
    139          */
    140         TileLoader makeTileLoader(TileLoaderListener listener);
    141 
    142         /**
    143          * @param listener object that will be notified, when tile has finished loading
    144          * @param headers HTTP headers that should be sent by TileLoader to tile server
    145          * @return TileLoader that will notify the listener
    146          */
    147         TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
    148     }
     43    /** loader factory responsible for loading tiles for this layer */
     44    public static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("TMS"){
    14945
    150     protected TileCache tileCache;
    151     protected TileSource tileSource;
    152     protected TileLoader tileLoader;
    153 
    154 
    155     public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
    15646        @Override
    157         public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
    158             Map<String, String> headers = new HashMap<>();
    159             headers.put("User-Agent", Version.getInstance().getFullAgentString());
    160             headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
    161             if (inputHeaders != null)
    162                 headers.putAll(inputHeaders);
    163 
    164             try {
    165                 return new TMSCachedTileLoader(listener, "TMS",
    166                         Main.pref.getInteger("socket.timeout.connect",15) * 1000,
    167                         Main.pref.getInteger("socket.timeout.read", 30) * 1000,
    168                         headers,
    169                         PROP_TILECACHE_DIR.get());
    170             } catch (IOException e) {
    171                 Main.warn(e);
    172             }
    173             return null;
     47        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
     48                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
     49            return new TMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
    17450        }
    17551
    176         @Override
    177         public TileLoader makeTileLoader(TileLoaderListener listener) {
    178             return makeTileLoader(listener, null);
    179         }
    18052    };
    18153
    18254    /**
    183      * Plugins that wish to set custom tile loader should call this method
     55     * Create a layer based on ImageryInfo
     56     * @param info description of the layer
    18457     */
    185 
    186     public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
    187         TMSLayer.loaderFactory = loaderFactory;
    188     }
    189 
    190     @Override
    191     public synchronized void tileLoadingFinished(Tile tile, boolean success) {
    192         if (tile.hasError()) {
    193             success = false;
    194             tile.setImage(null);
    195         }
    196         if (sharpenLevel != 0 && success) {
    197             tile.setImage(sharpenImage(tile.getImage()));
    198         }
    199         tile.setLoaded(success);
    200         needRedraw = true;
    201         if (Main.map != null) {
    202             Main.map.repaint(100);
    203         }
    204         if (Main.isDebugEnabled()) {
    205             Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
    206         }
    207     }
    208 
    209     /**
    210      * Clears the tile cache.
    211      *
    212      * If the current tileLoader is an instance of OsmTileLoader, a new
    213      * TmsTileClearController is created and passed to the according clearCache
    214      * method.
    215      *
    216      * @param monitor not used in this implementation - as cache clear is instaneus
    217      */
    218     public void clearTileCache(ProgressMonitor monitor) {
    219         tileCache.clear();
    220         if (tileLoader instanceof CachedTileLoader) {
    221             ((CachedTileLoader)tileLoader).clearCache(tileSource);
    222         }
    223         redraw();
     58    public TMSLayer(ImageryInfo info) {
     59        super(info);
    22460    }
    22561
    22662    /**
    227      * Zoomlevel at which tiles is currently downloaded.
    228      * Initial zoom lvl is set to bestZoom
    229      */
    230     public int currentZoomLevel;
    231 
    232     private Tile clickedTile;
    233     private boolean needRedraw;
    234     private JPopupMenu tileOptionMenu;
    235     private JCheckBoxMenuItem autoZoomPopup;
    236     private JCheckBoxMenuItem autoLoadPopup;
    237     private JCheckBoxMenuItem showErrorsPopup;
    238     private Tile showMetadataTile;
    239     private AttributionSupport attribution = new AttributionSupport();
    240     private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
    241 
    242     protected boolean autoZoom;
    243     protected boolean autoLoad;
    244     protected boolean showErrors;
    245 
    246     /**
    247      * Initiates a repaint of Main.map
    248      *
    249      * @see Main#map
    250      * @see MapFrame#repaint()
     63     * Plugins that wish to set custom tile loader should call this method
     64     * @param newLoaderFactory that will be used to load tiles
    25165     */
    252     protected void redraw() {
    253         needRedraw = true;
    254         Main.map.repaint();
    255     }
    256 
    257     protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
    258         if(maxZoomLvl > MAX_ZOOM) {
    259             maxZoomLvl = MAX_ZOOM;
    260         }
    261         if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
    262             maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
    263         }
    264         if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
    265             maxZoomLvl = ts.getMaxZoom();
    266         }
    267         return maxZoomLvl;
    268     }
    269 
    270     public static int getMaxZoomLvl(TileSource ts) {
    271         return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
    272     }
    273 
    274     public static void setMaxZoomLvl(int maxZoomLvl) {
    275         Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));
    276         PROP_MAX_ZOOM_LVL.put(newMaxZoom);
    277     }
    27866
    279     static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
    280         if(minZoomLvl < MIN_ZOOM) {
    281             /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
    282             minZoomLvl = MIN_ZOOM;
    283         }
    284         if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
    285             /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
    286             minZoomLvl = getMaxZoomLvl(ts);
    287         }
    288         if (ts != null && ts.getMinZoom() > minZoomLvl) {
    289             /*Main.debug("Increasing min. zoom level to match tile source");*/
    290             minZoomLvl = ts.getMinZoom();
    291         }
    292         return minZoomLvl;
    293     }
    294 
    295     public static int getMinZoomLvl(TileSource ts) {
    296         return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
    297     }
    298 
    299     public static void setMinZoomLvl(int minZoomLvl) {
    300         minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
    301         PROP_MIN_ZOOM_LVL.put(minZoomLvl);
    302     }
    303 
    304     private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
    305 
    306         public CachedAttributionBingAerialTileSource(ImageryInfo info) {
    307             super(info);
    308         }
    309 
    310         class BingAttributionData extends CacheCustomContent<IOException> {
    311 
    312             public BingAttributionData() {
    313                 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
     67    public static void setTileLoaderFactory(TileLoaderFactory newLoaderFactory) {
     68        loaderFactory = newLoaderFactory;
    31469    }
    31570
    31671    @Override
    317             protected byte[] updateData() throws IOException {
    318                 URL u = getAttributionUrl();
    319                 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
    320                     String r = scanner.useDelimiter("\\A").next();
    321                     Main.info("Successfully loaded Bing attribution data.");
    322                     return r.getBytes("UTF-8");
    323                 }
     72    protected TileLoaderFactory getTileLoaderFactory() {
     73        return loaderFactory;
    32474    }
    325         }
    326 
    327         @Override
    328         protected Callable<List<Attribution>> getAttributionLoaderCallable() {
    329             return new Callable<List<Attribution>>() {
    33075
    33176    @Override
    332                 public List<Attribution> call() throws Exception {
    333                     BingAttributionData attributionLoader = new BingAttributionData();
    334                     int waitTimeSec = 1;
    335                     while (true) {
    336                         try {
    337                             String xml = attributionLoader.updateIfRequiredString();
    338                             return parseAttributionText(new InputSource(new StringReader(xml)));
    339                         } catch (IOException ex) {
    340                             Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
    341                             Thread.sleep(waitTimeSec * 1000L);
    342                             waitTimeSec *= 2;
    343                         }
    344                     }
    345                 }
    346             };
     77    protected Map<String, String> getHeaders(TileSource tileSource) {
     78        if (tileSource instanceof TemplatedTMSTileSource) {
     79            return ((TemplatedTMSTileSource)tileSource).getHeaders();
    34780        }
     81        return null;
    34882    }
    34983
    35084    /**
    public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderL  
    35993     * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
    36094     * @throws IllegalArgumentException
    36195     */
    362     public static TileSource getTileSource(ImageryInfo info) {
    363         if (info.getImageryType() == ImageryType.TMS) {
    364             checkUrl(info.getUrl());
    365             TMSTileSource t = new TemplatedTMSTileSource(info);
    366             info.setAttribution(t);
    367             return t;
    368         } else if (info.getImageryType() == ImageryType.BING) {
    369             return new CachedAttributionBingAerialTileSource(info);
    370         } else if (info.getImageryType() == ImageryType.SCANEX) {
    371             return new ScanexTileSource(info);
    372         }
    373         return null;
    374     }
    375 
    376     /**
    377      * Checks validity of given URL.
    378      * @param url URL to check
    379      * @throws IllegalArgumentException if url is null or invalid
    380      */
    381     public static void checkUrl(String url) {
    382         CheckParameterUtil.ensureParameterNotNull(url, "url");
    383         Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
    384         while (m.find()) {
    385             boolean isSupportedPattern = false;
    386             for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
    387                 if (m.group().matches(pattern)) {
    388                     isSupportedPattern = true;
    389                     break;
    390                 }
    391             }
    392             if (!isSupportedPattern) {
    393                 throw new IllegalArgumentException(
    394                         tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
    395             }
    396         }
    397     }
    398 
    399     private void initTileSource(TileSource tileSource) {
    400         this.tileSource = tileSource;
    401         attribution.initialize(tileSource);
    402 
    403         currentZoomLevel = getBestZoom();
    404 
    405         Map<String, String> headers = null;
    406         if (tileSource instanceof TemplatedTMSTileSource) {
    407             headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
    408         }
    409 
    410         tileLoader = loaderFactory.makeTileLoader(this, headers);
    411         if (tileLoader instanceof TMSCachedTileLoader) {
    412             tileCache = (TileCache) tileLoader;
    413         } else {
    414             tileCache = new MemoryTileCache();
    415         }
    416         if (tileLoader == null)
    417             tileLoader = new OsmTileLoader(this);
    418     }
    419 
    420     /**
    421      * Marks layer as needing redraw on offset change
    422      */
    42396    @Override
    424     public void setOffset(double dx, double dy) {
    425         super.setOffset(dx, dy);
    426         needRedraw = true;
    427     }
    428     /**
    429      * Returns average number of screen pixels per tile pixel for current mapview
    430      */
    431     private double getScaleFactor(int zoom) {
    432         if (!Main.isDisplayingMapView()) return 1;
    433         MapView mv = Main.map.mapView;
    434         LatLon topLeft = mv.getLatLon(0, 0);
    435         LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
    436         double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
    437         double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
    438         double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
    439         double y2 = tileSource.latToTileY(botRight.lat(), zoom);
    440 
    441         int screenPixels = mv.getWidth()*mv.getHeight();
    442         double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
    443         if (screenPixels == 0 || tilePixels == 0) return 1;
    444         return screenPixels/tilePixels;
    445     }
    446 
    447     private final int getBestZoom() {
    448         double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
    449         double result = Math.log(factor)/Math.log(2)/2+1;
    450         /*
    451          * Math.log(factor)/Math.log(2) - gives log base 2 of factor
    452          * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
    453          * In general, smaller zoom levels are more readable.  We prefer big,
    454          * block, pixelated (but readable) map text to small, smeared,
    455          * unreadable underzoomed text.  So, use .floor() instead of rounding
    456          * to skew things a bit toward the lower zooms.
    457          * Remember, that result here, should correspond to TMSLayer.paint(...)
    458          * getScaleFactor(...) is supposed to be between 0.75 and 3
    459          */
    460         int intResult = (int)Math.floor(result);
    461         if (intResult > getMaxZoomLvl())
    462             return getMaxZoomLvl();
    463         if (intResult < getMinZoomLvl())
    464             return getMinZoomLvl();
    465         return intResult;
    466     }
    467 
    468     @SuppressWarnings("serial")
    469     public TMSLayer(ImageryInfo info) {
    470         super(info);
    471 
    472         if(!isProjectionSupported(Main.getProjection())) {
    473             JOptionPane.showMessageDialog(Main.parent,
    474                     tr("TMS layers do not support the projection {0}.\n{1}\n"
    475                             + "Change the projection or remove the layer.",
    476                             Main.getProjection().toCode(), nameSupportedProjections()),
    477                             tr("Warning"),
    478                             JOptionPane.WARNING_MESSAGE);
     97    protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
     98        return AbstractTMSTileSource.getTileSource(info);
    47999    }
    480100
    481         setBackgroundLayer(true);
    482         this.setVisible(true);
    483 
    484         TileSource source = getTileSource(info);
    485         if (source == null)
    486             throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
    487         initTileSource(source);
    488 
    489         MapView.addZoomChangeListener(this);
    490     }
    491101
    492102    /**
    493103     * Adds a context menu to the mapView.
    494104     */
    495     @Override
    496     public void hookUpMapView() {
    497         tileOptionMenu = new JPopupMenu();
    498 
    499         autoZoom = PROP_DEFAULT_AUTOZOOM.get();
    500         autoZoomPopup = new JCheckBoxMenuItem();
    501         autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
    502             @Override
    503             public void actionPerformed(ActionEvent ae) {
    504                 autoZoom = !autoZoom;
    505             }
    506         });
    507         autoZoomPopup.setSelected(autoZoom);
    508         tileOptionMenu.add(autoZoomPopup);
    509 
    510         autoLoad = PROP_DEFAULT_AUTOLOAD.get();
    511         autoLoadPopup = new JCheckBoxMenuItem();
    512         autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
    513             @Override
    514             public void actionPerformed(ActionEvent ae) {
    515                 autoLoad= !autoLoad;
    516             }
    517         });
    518         autoLoadPopup.setSelected(autoLoad);
    519         tileOptionMenu.add(autoLoadPopup);
    520 
    521         showErrors = PROP_DEFAULT_SHOWERRORS.get();
    522         showErrorsPopup = new JCheckBoxMenuItem();
    523         showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
    524             @Override
    525             public void actionPerformed(ActionEvent ae) {
    526                 showErrors = !showErrors;
    527             }
    528         });
    529         showErrorsPopup.setSelected(showErrors);
    530         tileOptionMenu.add(showErrorsPopup);
    531 
    532         tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
    533             @Override
    534             public void actionPerformed(ActionEvent ae) {
    535                 if (clickedTile != null) {
    536                     loadTile(clickedTile, true);
    537                     redraw();
    538                 }
    539             }
    540         }));
    541 
    542         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    543                 tr("Show Tile Info")) {
    544             private String getSizeString(int size) {
    545                 StringBuilder ret = new StringBuilder();
    546                 return ret.append(size).append("x").append(size).toString();
    547             }
    548 
    549             private JTextField createTextField(String text) {
    550                 JTextField ret = new JTextField(text);
    551                 ret.setEditable(false);
    552                 ret.setBorder(BorderFactory.createEmptyBorder());
    553                 return ret;
    554             }
    555             @Override
    556             public void actionPerformed(ActionEvent ae) {
    557                 if (clickedTile != null) {
    558                     ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
    559                     JPanel panel = new JPanel(new GridBagLayout());
    560                     Rectangle displaySize = tileToRect(clickedTile);
    561                     String url = "";
    562                     try {
    563                         url = clickedTile.getUrl();
    564                     } catch (IOException e) {
    565                         // silence exceptions
    566                     }
    567 
    568                     String[][] content = {
    569                             {"Tile name", clickedTile.getKey()},
    570                             {"Tile url", url},
    571                             {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
    572                             {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
    573                     };
    574 
    575                     for (String[] entry: content) {
    576                         panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
    577                         panel.add(GBC.glue(5,0), GBC.std());
    578                         panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
    579                     }
    580 
    581                     for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
    582                         panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
    583                         panel.add(GBC.glue(5,0), GBC.std());
    584                         String value = e.getValue();
    585                         if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
    586                             value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
    587                         }
    588                         panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
    589 
    590                     }
    591                     ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
    592                     ed.setContent(panel);
    593                     ed.showDialog();
    594                 }
    595             }
    596         }));
    597 
    598         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    599                 tr("Request Update")) {
    600             @Override
    601             public void actionPerformed(ActionEvent ae) {
    602                 if (clickedTile != null) {
    603                     clickedTile.setLoaded(false);
    604                     tileLoader.createTileLoaderJob(clickedTile).submit();
    605                 }
    606             }
    607         }));
    608 
    609         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    610                 tr("Load All Tiles")) {
    611             @Override
    612             public void actionPerformed(ActionEvent ae) {
    613                 loadAllTiles(true);
    614                 redraw();
    615             }
    616         }));
    617 
    618         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    619                 tr("Load All Error Tiles")) {
    620             @Override
    621             public void actionPerformed(ActionEvent ae) {
    622                 loadAllErrorTiles(true);
    623                 redraw();
    624             }
    625         }));
    626 
    627         // increase and decrease commands
    628         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    629                 tr("Increase zoom")) {
    630             @Override
    631             public void actionPerformed(ActionEvent ae) {
    632                 increaseZoomLevel();
    633                 redraw();
    634             }
    635         }));
    636 
    637         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    638                 tr("Decrease zoom")) {
    639             @Override
    640             public void actionPerformed(ActionEvent ae) {
    641                 decreaseZoomLevel();
    642                 redraw();
    643             }
    644         }));
    645 
    646         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    647                 tr("Snap to tile size")) {
    648             @Override
    649             public void actionPerformed(ActionEvent ae) {
    650                 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
    651                 Main.map.mapView.zoomToFactor(newFactor);
    652                 redraw();
    653             }
    654         }));
    655 
    656         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    657                 tr("Flush Tile Cache")) {
    658             @Override
    659             public void actionPerformed(ActionEvent ae) {
    660                 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
    661                     @Override
    662                     protected void realRun() throws SAXException, IOException,
    663                             OsmTransferException {
    664                         clearTileCache(getProgressMonitor());
    665                     }
    666 
    667                     @Override
    668                     protected void finish() {
    669                     }
    670 
    671                     @Override
    672                     protected void cancel() {
    673                     }
    674                 }.run();
    675             }
    676         }));
    677 
    678         final MouseAdapter adapter = new MouseAdapter() {
    679             @Override
    680             public void mouseClicked(MouseEvent e) {
    681                 if (!isVisible()) return;
    682                 if (e.getButton() == MouseEvent.BUTTON3) {
    683                     clickedTile = getTileForPixelpos(e.getX(), e.getY());
    684                     tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
    685                 } else if (e.getButton() == MouseEvent.BUTTON1) {
    686                     attribution.handleAttribution(e.getPoint(), true);
    687                 }
    688             }
    689         };
    690         Main.map.mapView.addMouseListener(adapter);
    691 
    692         MapView.addLayerChangeListener(new LayerChangeListener() {
    693             @Override
    694             public void activeLayerChange(Layer oldLayer, Layer newLayer) {
    695                 //
    696             }
    697 
    698             @Override
    699             public void layerAdded(Layer newLayer) {
    700                 //
    701             }
    702 
    703             @Override
    704             public void layerRemoved(Layer oldLayer) {
    705                 if (oldLayer == TMSLayer.this) {
    706                     Main.map.mapView.removeMouseListener(adapter);
    707                     MapView.removeZoomChangeListener(TMSLayer.this);
    708                     MapView.removeLayerChangeListener(this);
    709                 }
    710             }
    711         });
    712     }
    713 
    714     /**
    715      * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
    716      * changes to visible map (panning/zooming)
    717      */
    718     @Override
    719     public void zoomChanged() {
    720         if (Main.isDebugEnabled()) {
    721             Main.debug("zoomChanged(): " + currentZoomLevel);
    722         }
    723         needRedraw = true;
    724         if (tileLoader instanceof TMSCachedTileLoader) {
    725             ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
    726         }
    727     }
    728 
    729     protected int getMaxZoomLvl() {
    730         if (info.getMaxZoom() != 0)
    731             return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
    732         else
    733             return getMaxZoomLvl(tileSource);
    734     }
    735 
    736     protected int getMinZoomLvl() {
    737         return getMinZoomLvl(tileSource);
    738     }
    739 
    740     /**
    741      * Zoom in, go closer to map.
    742      *
    743      * @return    true, if zoom increasing was successful, false otherwise
    744      */
    745     public boolean zoomIncreaseAllowed() {
    746         boolean zia = currentZoomLevel < this.getMaxZoomLvl();
    747         if (Main.isDebugEnabled()) {
    748             Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
    749         }
    750         return zia;
    751     }
    752 
    753     public boolean increaseZoomLevel() {
    754         if (zoomIncreaseAllowed()) {
    755             currentZoomLevel++;
    756             if (Main.isDebugEnabled()) {
    757                 Main.debug("increasing zoom level to: " + currentZoomLevel);
    758             }
    759             zoomChanged();
    760         } else {
    761             Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
    762                     "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
    763             return false;
    764         }
    765         return true;
    766     }
    767 
    768     public boolean setZoomLevel(int zoom) {
    769         if (zoom == currentZoomLevel) return true;
    770         if (zoom > this.getMaxZoomLvl()) return false;
    771         if (zoom < this.getMinZoomLvl()) return false;
    772         currentZoomLevel = zoom;
    773         zoomChanged();
    774         return true;
    775     }
    776 
    777     /**
    778      * Check if zooming out is allowed
    779      *
    780      * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
    781      */
    782     public boolean zoomDecreaseAllowed() {
    783         return currentZoomLevel > this.getMinZoomLvl();
    784     }
    785 
    786     /**
    787      * Zoom out from map.
    788      *
    789      * @return    true, if zoom increasing was successfull, false othervise
    790      */
    791     public boolean decreaseZoomLevel() {
    792         //int minZoom = this.getMinZoomLvl();
    793         if (zoomDecreaseAllowed()) {
    794             if (Main.isDebugEnabled()) {
    795                 Main.debug("decreasing zoom level to: " + currentZoomLevel);
    796             }
    797             currentZoomLevel--;
    798             zoomChanged();
    799         } else {
    800             /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
    801             return false;
    802         }
    803         return true;
    804     }
    805 
    806     /*
    807      * We use these for quick, hackish calculations.  They
    808      * are temporary only and intentionally not inserted
    809      * into the tileCache.
    810      */
    811     private Tile tempCornerTile(Tile t) {
    812         int x = t.getXtile() + 1;
    813         int y = t.getYtile() + 1;
    814         int zoom = t.getZoom();
    815         Tile tile = getTile(x, y, zoom);
    816         if (tile != null)
    817             return tile;
    818         return new Tile(tileSource, x, y, zoom);
    819     }
    820 
    821     private Tile getOrCreateTile(int x, int y, int zoom) {
    822         Tile tile = getTile(x, y, zoom);
    823         if (tile == null) {
    824             tile = new Tile(tileSource, x, y, zoom);
    825             tileCache.addTile(tile);
    826             tile.loadPlaceholderFromCache(tileCache);
    827         }
    828         return tile;
    829     }
    830 
    831     /*
    832      * This can and will return null for tiles that are not
    833      * already in the cache.
    834      */
    835     private Tile getTile(int x, int y, int zoom) {
    836         int max = 1 << zoom;
    837         if (x < 0 || x >= max || y < 0 || y >= max)
    838             return null;
    839         return tileCache.getTile(tileSource, x, y, zoom);
    840     }
    841 
    842     private boolean loadTile(Tile tile, boolean force) {
    843         if (tile == null)
    844             return false;
    845         if (!force && (tile.isLoaded() || tile.hasError()))
    846             return false;
    847         if (tile.isLoading())
    848             return false;
    849         tileLoader.createTileLoaderJob(tile).submit();
    850         return true;
    851     }
    852 
    853     private void loadAllTiles(boolean force) {
    854         MapView mv = Main.map.mapView;
    855         EastNorth topLeft = mv.getEastNorth(0, 0);
    856         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    857 
    858         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
    859 
    860         // if there is more than 18 tiles on screen in any direction, do not
    861         // load all tiles!
    862         if (ts.tooLarge()) {
    863             Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
    864             return;
    865         }
    866         ts.loadAllTiles(force);
    867     }
    868 
    869     private void loadAllErrorTiles(boolean force) {
    870         MapView mv = Main.map.mapView;
    871         EastNorth topLeft = mv.getEastNorth(0, 0);
    872         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    873 
    874         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
    875 
    876         ts.loadAllErrorTiles(force);
    877     }
    878 
    879     @Override
    880     public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    881         boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
    882         needRedraw = true;
    883         if (Main.isDebugEnabled()) {
    884             Main.debug("imageUpdate() done: " + done + " calling repaint");
    885         }
    886         Main.map.repaint(done ? 0 : 100);
    887         return !done;
    888     }
    889 
    890     private boolean imageLoaded(Image i) {
    891         if (i == null)
    892             return false;
    893         int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
    894         if ((status & ALLBITS) != 0)
    895             return true;
    896         return false;
    897     }
    898 
    899     /**
    900      * Returns the image for the given tile if both tile and image are loaded.
    901      * Otherwise returns  null.
    902      *
    903      * @param tile the Tile for which the image should be returned
    904      * @return  the image of the tile or null.
    905      */
    906     private Image getLoadedTileImage(Tile tile) {
    907         if (!tile.isLoaded())
    908             return null;
    909         Image img = tile.getImage();
    910         if (!imageLoaded(img))
    911             return null;
    912         return img;
    913     }
    914 
    915     private LatLon tileLatLon(Tile t) {
    916         int zoom = t.getZoom();
    917         return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
    918                 tileSource.tileXToLon(t.getXtile(), zoom));
    919     }
    920 
    921     private Rectangle tileToRect(Tile t1) {
    922         /*
    923          * We need to get a box in which to draw, so advance by one tile in
    924          * each direction to find the other corner of the box.
    925          * Note: this somewhat pollutes the tile cache
    926          */
    927         Tile t2 = tempCornerTile(t1);
    928         Rectangle rect = new Rectangle(pixelPos(t1));
    929         rect.add(pixelPos(t2));
    930         return rect;
    931     }
    932 
    933     // 'source' is the pixel coordinates for the area that
    934     // the img is capable of filling in.  However, we probably
    935     // only want a portion of it.
    936     //
    937     // 'border' is the screen cordinates that need to be drawn.
    938     //  We must not draw outside of it.
    939     private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
    940         Rectangle target = source;
    941 
    942         // If a border is specified, only draw the intersection
    943         // if what we have combined with what we are supposed
    944         // to draw.
    945         if (border != null) {
    946             target = source.intersection(border);
    947             if (Main.isDebugEnabled()) {
    948                 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
    949             }
    950         }
    951 
    952         // All of the rectangles are in screen coordinates.  We need
    953         // to how these correlate to the sourceImg pixels.  We could
    954         // avoid doing this by scaling the image up to the 'source' size,
    955         // but this should be cheaper.
    956         //
    957         // In some projections, x any y are scaled differently enough to
    958         // cause a pixel or two of fudge.  Calculate them separately.
    959         double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
    960         double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
    961 
    962         // How many pixels into the 'source' rectangle are we drawing?
    963         int screen_x_offset = target.x - source.x;
    964         int screen_y_offset = target.y - source.y;
    965         // And how many pixels into the image itself does that
    966         // correlate to?
    967         int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
    968         int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
    969         // Now calculate the other corner of the image that we need
    970         // by scaling the 'target' rectangle's dimensions.
    971         int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
    972         int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
    973 
    974         if (Main.isDebugEnabled()) {
    975             Main.debug("drawing image into target rect: " + target);
    976         }
    977         g.drawImage(sourceImg,
    978                 target.x, target.y,
    979                 target.x + target.width, target.y + target.height,
    980                 img_x_offset, img_y_offset,
    981                 img_x_end, img_y_end,
    982                 this);
    983         if (PROP_FADE_AMOUNT.get() != 0) {
    984             // dimm by painting opaque rect...
    985             g.setColor(getFadeColorWithAlpha());
    986             g.fillRect(target.x, target.y,
    987                     target.width, target.height);
    988         }
    989     }
    990 
    991     // This function is called for several zoom levels, not just
    992     // the current one.  It should not trigger any tiles to be
    993     // downloaded.  It should also avoid polluting the tile cache
    994     // with any tiles since these tiles are not mandatory.
    995     //
    996     // The "border" tile tells us the boundaries of where we may
    997     // draw.  It will not be from the zoom level that is being
    998     // drawn currently.  If drawing the displayZoomLevel,
    999     // border is null and we draw the entire tile set.
    1000     private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
    1001         if (zoom <= 0) return Collections.emptyList();
    1002         Rectangle borderRect = null;
    1003         if (border != null) {
    1004             borderRect = tileToRect(border);
    1005         }
    1006         List<Tile> missedTiles = new LinkedList<>();
    1007         // The callers of this code *require* that we return any tiles
    1008         // that we do not draw in missedTiles.  ts.allExistingTiles() by
    1009         // default will only return already-existing tiles.  However, we
    1010         // need to return *all* tiles to the callers, so force creation
    1011         // here.
    1012         //boolean forceTileCreation = true;
    1013         for (Tile tile : ts.allTilesCreate()) {
    1014             Image img = getLoadedTileImage(tile);
    1015             if (img == null || tile.hasError()) {
    1016                 if (Main.isDebugEnabled()) {
    1017                     Main.debug("missed tile: " + tile);
    1018                 }
    1019                 missedTiles.add(tile);
    1020                 continue;
    1021             }
    1022             Rectangle sourceRect = tileToRect(tile);
    1023             if (borderRect != null && !sourceRect.intersects(borderRect)) {
    1024                 continue;
    1025             }
    1026             drawImageInside(g, img, sourceRect, borderRect);
    1027         }
    1028         return missedTiles;
    1029     }
    1030 
    1031     private void myDrawString(Graphics g, String text, int x, int y) {
    1032         Color oldColor = g.getColor();
    1033         g.setColor(Color.black);
    1034         g.drawString(text,x+1,y+1);
    1035         g.setColor(oldColor);
    1036         g.drawString(text,x,y);
    1037     }
    1038 
    1039     private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
    1040         int fontHeight = g.getFontMetrics().getHeight();
    1041         if (tile == null)
    1042             return;
    1043         Point p = pixelPos(t);
    1044         int texty = p.y + 2 + fontHeight;
    1045 
    1046         /*if (PROP_DRAW_DEBUG.get()) {
    1047             myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
    1048             texty += 1 + fontHeight;
    1049             if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
    1050                 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
    1051                 texty += 1 + fontHeight;
    1052             }
    1053         }*/
    1054 
    1055         if (tile == showMetadataTile) {
    1056             String md = tile.toString();
    1057             if (md != null) {
    1058                 myDrawString(g, md, p.x + 2, texty);
    1059                 texty += 1 + fontHeight;
    1060             }
    1061             Map<String, String> meta = tile.getMetadata();
    1062             if (meta != null) {
    1063                 for (Map.Entry<String, String> entry : meta.entrySet()) {
    1064                     myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
    1065                     texty += 1 + fontHeight;
    1066                 }
    1067             }
    1068         }
    1069 
    1070         /*String tileStatus = tile.getStatus();
    1071         if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
    1072             myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
    1073             texty += 1 + fontHeight;
    1074         }*/
    1075 
    1076         if (tile.hasError() && showErrors) {
    1077             myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
    1078             texty += 1 + fontHeight;
    1079         }
    1080 
    1081         /*int xCursor = -1;
    1082         int yCursor = -1;
    1083         if (PROP_DRAW_DEBUG.get()) {
    1084             if (yCursor < t.getYtile()) {
    1085                 if (t.getYtile() % 32 == 31) {
    1086                     g.fillRect(0, p.y - 1, mv.getWidth(), 3);
    1087                 } else {
    1088                     g.drawLine(0, p.y, mv.getWidth(), p.y);
    1089                 }
    1090                 yCursor = t.getYtile();
    1091             }
    1092             // This draws the vertical lines for the entire
    1093             // column. Only draw them for the top tile in
    1094             // the column.
    1095             if (xCursor < t.getXtile()) {
    1096                 if (t.getXtile() % 32 == 0) {
    1097                     // level 7 tile boundary
    1098                     g.fillRect(p.x - 1, 0, 3, mv.getHeight());
    1099                 } else {
    1100                     g.drawLine(p.x, 0, p.x, mv.getHeight());
    1101                 }
    1102                 xCursor = t.getXtile();
    1103             }
    1104         }*/
    1105     }
    1106 
    1107     private Point pixelPos(LatLon ll) {
    1108         return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
    1109     }
    1110 
    1111     private Point pixelPos(Tile t) {
    1112         double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
    1113         LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
    1114         return pixelPos(tmpLL);
    1115     }
    1116 
    1117     private LatLon getShiftedLatLon(EastNorth en) {
    1118         return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
    1119     }
    1120 
    1121     private Coordinate getShiftedCoord(EastNorth en) {
    1122         LatLon ll = getShiftedLatLon(en);
    1123         return new Coordinate(ll.lat(),ll.lon());
    1124     }
    1125 
    1126     private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
    1127     private final class TileSet {
    1128         private int x0, x1, y0, y1;
    1129         private int zoom;
    1130         private int tileMax = -1;
    1131 
    1132         /**
    1133          * Create a TileSet by EastNorth bbox taking a layer shift in account
    1134          */
    1135         private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
    1136             this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
    1137         }
    1138 
    1139         /**
    1140          * Create a TileSet by known LatLon bbox without layer shift correction
    1141          */
    1142         private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
    1143             this.zoom = zoom;
    1144             if (zoom == 0)
    1145                 return;
    1146 
    1147             x0 = (int)tileSource.lonToTileX(topLeft.lon(),  zoom);
    1148             y0 = (int)tileSource.latToTileY(topLeft.lat(),  zoom);
    1149             x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
    1150             y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
    1151             if (x0 > x1) {
    1152                 int tmp = x0;
    1153                 x0 = x1;
    1154                 x1 = tmp;
    1155             }
    1156             if (y0 > y1) {
    1157                 int tmp = y0;
    1158                 y0 = y1;
    1159                 y1 = tmp;
    1160             }
    1161             tileMax = (int)Math.pow(2.0, zoom);
    1162             if (x0 < 0) {
    1163                 x0 = 0;
    1164             }
    1165             if (y0 < 0) {
    1166                 y0 = 0;
    1167             }
    1168             if (x1 > tileMax) {
    1169                 x1 = tileMax;
    1170             }
    1171             if (y1 > tileMax) {
    1172                 y1 = tileMax;
    1173             }
    1174         }
    1175 
    1176         private boolean tooSmall() {
    1177             return this.tilesSpanned() < 2.1;
    1178         }
    1179 
    1180         private boolean tooLarge() {
    1181             return this.tilesSpanned() > 10;
    1182         }
    1183 
    1184         private boolean insane() {
    1185             return this.tilesSpanned() > 100;
    1186         }
    1187 
    1188         private double tilesSpanned() {
    1189             return Math.sqrt(1.0 * this.size());
    1190         }
    1191 
    1192         private int size() {
    1193             int x_span = x1 - x0 + 1;
    1194             int y_span = y1 - y0 + 1;
    1195             return x_span * y_span;
    1196         }
    1197 
    1198         /*
    1199          * Get all tiles represented by this TileSet that are
    1200          * already in the tileCache.
    1201          */
    1202         private List<Tile> allExistingTiles() {
    1203             return this.__allTiles(false);
    1204         }
    1205 
    1206         private List<Tile> allTilesCreate() {
    1207             return this.__allTiles(true);
    1208         }
    1209 
    1210         private List<Tile> __allTiles(boolean create) {
    1211             // Tileset is either empty or too large
    1212             if (zoom == 0 || this.insane())
    1213                 return Collections.emptyList();
    1214             List<Tile> ret = new ArrayList<>();
    1215             for (int x = x0; x <= x1; x++) {
    1216                 for (int y = y0; y <= y1; y++) {
    1217                     Tile t;
    1218                     if (create) {
    1219                         t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
    1220                     } else {
    1221                         t = getTile(x % tileMax, y % tileMax, zoom);
    1222                     }
    1223                     if (t != null) {
    1224                         ret.add(t);
    1225                     }
    1226                 }
    1227             }
    1228             return ret;
    1229         }
    1230 
    1231         private List<Tile> allLoadedTiles() {
    1232             List<Tile> ret = new ArrayList<>();
    1233             for (Tile t : this.allExistingTiles()) {
    1234                 if (t.isLoaded())
    1235                     ret.add(t);
    1236             }
    1237             return ret;
    1238         }
    1239 
    1240         private Comparator<Tile> getTileDistanceComparator() {
    1241             final int centerX = (int) Math.ceil((x0 + x1) / 2);
    1242             final int centerY = (int) Math.ceil((y0 + y1) / 2);
    1243             return new Comparator<Tile>() {
    1244                 private int getDistance(Tile t) {
    1245                     return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
    1246                 }
    1247                 @Override
    1248                 public int compare(Tile o1, Tile o2) {
    1249                     int distance1 = getDistance(o1);
    1250                     int distance2 = getDistance(o2);
    1251                     return Integer.compare(distance1, distance2);
    1252                 }
    1253             };
    1254         }
    1255 
    1256         private void loadAllTiles(boolean force) {
    1257             if (!autoLoad && !force)
    1258                 return;
    1259             List<Tile> allTiles = allTilesCreate();
    1260             Collections.sort(allTiles, getTileDistanceComparator());
    1261             for (Tile t : allTiles) { //, getTileDistanceComparator())) {
    1262                 loadTile(t, false);
    1263             }
    1264         }
    1265 
    1266         private void loadAllErrorTiles(boolean force) {
    1267             if (!autoLoad && !force)
    1268                 return;
    1269             for (Tile t : this.allTilesCreate()) {
    1270                 if (t.hasError()) {
    1271                     loadTile(t, true);
    1272                 }
    1273             }
    1274         }
    1275     }
    1276 
    1277 
    1278     private static class TileSetInfo {
    1279         public boolean hasVisibleTiles = false;
    1280         public boolean hasOverzoomedTiles = false;
    1281         public boolean hasLoadingTiles = false;
    1282     }
    1283 
    1284     private static TileSetInfo getTileSetInfo(TileSet ts) {
    1285         List<Tile> allTiles = ts.allExistingTiles();
    1286         TileSetInfo result = new TileSetInfo();
    1287         result.hasLoadingTiles = allTiles.size() < ts.size();
    1288         for (Tile t : allTiles) {
    1289             if (t.isLoaded()) {
    1290                 if (!t.hasError()) {
    1291                     result.hasVisibleTiles = true;
    1292                 }
    1293                 if ("no-tile".equals(t.getValue("tile-info"))) {
    1294                     result.hasOverzoomedTiles = true;
    1295                 }
    1296             } else {
    1297                 result.hasLoadingTiles = true;
    1298             }
    1299         }
    1300         return result;
    1301     }
    1302 
    1303     private class DeepTileSet {
    1304         private final EastNorth topLeft, botRight;
    1305         private final int minZoom, maxZoom;
    1306         private final TileSet[] tileSets;
    1307         private final TileSetInfo[] tileSetInfos;
    1308         public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
    1309             this.topLeft = topLeft;
    1310             this.botRight = botRight;
    1311             this.minZoom = minZoom;
    1312             this.maxZoom = maxZoom;
    1313             this.tileSets = new TileSet[maxZoom - minZoom + 1];
    1314             this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
    1315         }
    1316         public TileSet getTileSet(int zoom) {
    1317             if (zoom < minZoom)
    1318                 return nullTileSet;
    1319             synchronized (tileSets) {
    1320                 TileSet ts = tileSets[zoom-minZoom];
    1321                 if (ts == null) {
    1322                     ts = new TileSet(topLeft, botRight, zoom);
    1323                     tileSets[zoom-minZoom] = ts;
    1324                 }
    1325                 return ts;
    1326             }
    1327         }
    1328 
    1329         public TileSetInfo getTileSetInfo(int zoom) {
    1330             if (zoom < minZoom)
    1331                 return new TileSetInfo();
    1332             synchronized (tileSetInfos) {
    1333                 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    1334                 if (tsi == null) {
    1335                     tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
    1336                     tileSetInfos[zoom-minZoom] = tsi;
    1337                 }
    1338                 return tsi;
    1339             }
    1340         }
    1341     }
    1342 
    1343     @Override
    1344     public void paint(Graphics2D g, MapView mv, Bounds bounds) {
    1345         EastNorth topLeft = mv.getEastNorth(0, 0);
    1346         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    1347 
    1348         if (botRight.east() == 0 || botRight.north() == 0) {
    1349             /*Main.debug("still initializing??");*/
    1350             // probably still initializing
    1351             return;
    1352         }
    1353 
    1354         needRedraw = false;
    1355 
    1356         int zoom = currentZoomLevel;
    1357         if (autoZoom) {
    1358             double pixelScaling = getScaleFactor(zoom);
    1359             if (pixelScaling > 3 || pixelScaling < 0.7) {
    1360                 zoom = getBestZoom();
    1361             }
    1362         }
    1363 
    1364         DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
    1365         TileSet ts = dts.getTileSet(zoom);
    1366 
    1367         int displayZoomLevel = zoom;
    1368 
    1369         boolean noTilesAtZoom = false;
    1370         if (autoZoom && autoLoad) {
    1371             // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
    1372             TileSetInfo tsi = dts.getTileSetInfo(zoom);
    1373             if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
    1374                 noTilesAtZoom = true;
    1375             }
    1376             // Find highest zoom level with at least one visible tile
    1377             for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
    1378                 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
    1379                     displayZoomLevel = tmpZoom;
    1380                     break;
    1381                 }
    1382             }
    1383             // Do binary search between currentZoomLevel and displayZoomLevel
    1384             while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
    1385                 zoom = (zoom + displayZoomLevel)/2;
    1386                 tsi = dts.getTileSetInfo(zoom);
    1387             }
    1388 
    1389             setZoomLevel(zoom);
    1390 
    1391             // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
    1392             // to make sure there're really no more zoom levels
    1393             if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
    1394                 zoom++;
    1395                 tsi = dts.getTileSetInfo(zoom);
    1396             }
    1397             // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
    1398             // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
    1399             while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
    1400                 zoom--;
    1401                 tsi = dts.getTileSetInfo(zoom);
    1402             }
    1403             ts = dts.getTileSet(zoom);
    1404         } else if (autoZoom) {
    1405             setZoomLevel(zoom);
    1406         }
    1407 
    1408         // Too many tiles... refuse to download
    1409         if (!ts.tooLarge()) {
    1410             //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
    1411             ts.loadAllTiles(false);
    1412         }
    1413 
    1414         if (displayZoomLevel != zoom) {
    1415             ts = dts.getTileSet(displayZoomLevel);
    1416         }
    1417 
    1418         g.setColor(Color.DARK_GRAY);
    1419 
    1420         List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
    1421         int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};
    1422         for (int zoomOffset : otherZooms) {
    1423             if (!autoZoom) {
    1424                 break;
    1425             }
    1426             int newzoom = displayZoomLevel + zoomOffset;
    1427             if (newzoom < MIN_ZOOM) {
    1428                 continue;
    1429             }
    1430             if (missedTiles.isEmpty()) {
    1431                 break;
    1432             }
    1433             List<Tile> newlyMissedTiles = new LinkedList<>();
    1434             for (Tile missed : missedTiles) {
    1435                 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
    1436                     // Don't try to paint from higher zoom levels when tile is overzoomed
    1437                     newlyMissedTiles.add(missed);
    1438                     continue;
    1439                 }
    1440                 Tile t2 = tempCornerTile(missed);
    1441                 LatLon topLeft2  = tileLatLon(missed);
    1442                 LatLon botRight2 = tileLatLon(t2);
    1443                 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
    1444                 // Instantiating large TileSets is expensive.  If there
    1445                 // are no loaded tiles, don't bother even trying.
    1446                 if (ts2.allLoadedTiles().isEmpty()) {
    1447                     newlyMissedTiles.add(missed);
    1448                     continue;
    1449                 }
    1450                 if (ts2.tooLarge()) {
    1451                     continue;
    1452                 }
    1453                 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
    1454             }
    1455             missedTiles = newlyMissedTiles;
    1456         }
    1457         if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
    1458             Main.debug("still missed "+missedTiles.size()+" in the end");
    1459         }
    1460         g.setColor(Color.red);
    1461         g.setFont(InfoFont);
    1462 
    1463         // The current zoom tileset should have all of its tiles
    1464         // due to the loadAllTiles(), unless it to tooLarge()
    1465         for (Tile t : ts.allExistingTiles()) {
    1466             this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
    1467         }
    1468 
    1469         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
    1470 
    1471         //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
    1472         g.setColor(Color.lightGray);
    1473         if (!autoZoom) {
    1474             if (ts.insane()) {
    1475                 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
    1476             } else if (ts.tooLarge()) {
    1477                 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
    1478             } else if (ts.tooSmall()) {
    1479                 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
    1480             }
    1481         }
    1482         if (noTilesAtZoom) {
    1483             myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
    1484         }
    1485         if (Main.isDebugEnabled()) {
    1486             myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
    1487             myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
    1488             myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
    1489             myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
    1490             if(tileLoader instanceof TMSCachedTileLoader) {
    1491                 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
    1492                 int offset = 185;
    1493                 for(String part: cachedTileLoader.getStats().split("\n")) {
    1494                     myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
    1495                 }
    1496 
    1497             }
    1498         }
    1499     }
    1500 
    1501     /**
    1502      * This isn't very efficient, but it is only used when the
    1503      * user right-clicks on the map.
    1504      */
    1505     private Tile getTileForPixelpos(int px, int py) {
    1506         if (Main.isDebugEnabled()) {
    1507             Main.debug("getTileForPixelpos("+px+", "+py+")");
    1508         }
    1509         MapView mv = Main.map.mapView;
    1510         Point clicked = new Point(px, py);
    1511         EastNorth topLeft = mv.getEastNorth(0, 0);
    1512         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    1513         int z = currentZoomLevel;
    1514         TileSet ts = new TileSet(topLeft, botRight, z);
    1515 
    1516         if (!ts.tooLarge()) {
    1517             ts.loadAllTiles(false); // make sure there are tile objects for all tiles
    1518         }
    1519         Tile clickedTile = null;
    1520         for (Tile t1 : ts.allExistingTiles()) {
    1521             Tile t2 = tempCornerTile(t1);
    1522             Rectangle r = new Rectangle(pixelPos(t1));
    1523             r.add(pixelPos(t2));
    1524             if (Main.isDebugEnabled()) {
    1525                 Main.debug("r: " + r + " clicked: " + clicked);
    1526             }
    1527             if (!r.contains(clicked)) {
    1528                 continue;
    1529             }
    1530             clickedTile  = t1;
    1531             break;
    1532         }
    1533         if (clickedTile == null)
    1534             return null;
    1535         /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
    1536                 " currentZoomLevel: " + currentZoomLevel);*/
    1537         return clickedTile;
    1538     }
    1539 
    1540     @Override
    1541     public Action[] getMenuEntries() {
    1542         return new Action[] {
    1543                 LayerListDialog.getInstance().createShowHideLayerAction(),
    1544                 LayerListDialog.getInstance().createDeleteLayerAction(),
    1545                 SeparatorLayerAction.INSTANCE,
    1546                 // color,
    1547                 new OffsetAction(),
    1548                 new RenameLayerAction(this.getAssociatedFile(), this),
    1549                 SeparatorLayerAction.INSTANCE,
    1550                 new LayerListPopup.InfoAction(this) };
    1551     }
    1552 
    1553     @Override
    1554     public String getToolTipText() {
    1555         return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
    1556     }
    1557 
    1558     @Override
    1559     public void visitBoundingBox(BoundingXYVisitor v) {
    1560     }
    1561 
    1562     @Override
    1563     public boolean isChanged() {
    1564         return needRedraw;
    1565     }
    1566105
    1567106    @Override
    1568107    public final boolean isProjectionSupported(Projection proj) {
  • 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 3bc2a12..443a07b 100644
    a b package org.openstreetmap.josm.gui.layer;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.Component;
    7 import java.awt.Graphics;
    8 import java.awt.Graphics2D;
    9 import java.awt.Image;
    10 import java.awt.Point;
    116import java.awt.event.ActionEvent;
    12 import java.awt.event.MouseAdapter;
    13 import java.awt.event.MouseEvent;
    14 import java.awt.image.BufferedImage;
    15 import java.awt.image.ImageObserver;
    16 import java.io.Externalizable;
    17 import java.io.File;
    187import java.io.IOException;
    19 import java.io.InvalidClassException;
    20 import java.io.ObjectInput;
    21 import java.io.ObjectOutput;
    228import java.util.ArrayList;
    23 import java.util.Collections;
    24 import java.util.HashSet;
    25 import java.util.Iterator;
     9import java.util.Arrays;
    2610import java.util.List;
    27 import java.util.Locale;
    28 import java.util.Set;
    29 import java.util.concurrent.locks.Condition;
    30 import java.util.concurrent.locks.Lock;
    31 import java.util.concurrent.locks.ReentrantLock;
     11import java.util.Map;
    3212
    3313import javax.swing.AbstractAction;
    3414import javax.swing.Action;
    35 import javax.swing.JCheckBoxMenuItem;
    36 import javax.swing.JMenuItem;
    37 import javax.swing.JOptionPane;
    3815
    39 import org.openstreetmap.gui.jmapviewer.AttributionSupport;
    40 import org.openstreetmap.josm.Main;
    41 import org.openstreetmap.josm.actions.SaveActionBase;
    42 import org.openstreetmap.josm.data.Bounds;
    43 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
    44 import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
    45 import org.openstreetmap.josm.data.ProjectionBounds;
    46 import org.openstreetmap.josm.data.coor.EastNorth;
    47 import org.openstreetmap.josm.data.coor.LatLon;
    48 import org.openstreetmap.josm.data.imagery.GeorefImage;
    49 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
     16import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     17import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     18import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     19import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
    5020import org.openstreetmap.josm.data.imagery.ImageryInfo;
    5121import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    5222import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
    53 import org.openstreetmap.josm.data.imagery.WmsCache;
    54 import org.openstreetmap.josm.data.imagery.types.ObjectFactory;
    55 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     23import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
     24import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     25import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
    5626import org.openstreetmap.josm.data.preferences.BooleanProperty;
    5727import org.openstreetmap.josm.data.preferences.IntegerProperty;
    58 import org.openstreetmap.josm.data.projection.Projection;
    59 import org.openstreetmap.josm.gui.MapView;
    60 import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
    61 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    62 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    63 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    64 import org.openstreetmap.josm.io.WMSLayerImporter;
    65 import org.openstreetmap.josm.io.imagery.HTMLGrabber;
    66 import org.openstreetmap.josm.io.imagery.WMSException;
    67 import org.openstreetmap.josm.io.imagery.WMSGrabber;
    68 import org.openstreetmap.josm.io.imagery.WMSRequest;
    69 import org.openstreetmap.josm.tools.ImageProvider;
    70 import org.openstreetmap.josm.tools.Utils;
    7128
    7229/**
    7330 * This is a layer that grabs the current screen from an WMS server. The data
    7431 * fetched this way is tiled and managed to the disc to reduce server load.
     32 *
    7533 */
    76 public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener, Externalizable {
    77 
    78     public static class PrecacheTask {
    79         private final ProgressMonitor progressMonitor;
    80         private volatile int totalCount;
    81         private volatile int processedCount;
    82         private volatile boolean isCancelled;
    83 
    84         public PrecacheTask(ProgressMonitor progressMonitor) {
    85             this.progressMonitor = progressMonitor;
    86         }
    87 
    88         public boolean isFinished() {
    89             return totalCount == processedCount;
    90         }
    91 
    92         public int getTotalCount() {
    93             return totalCount;
    94         }
    95 
    96         public void cancel() {
    97             isCancelled = true;
    98         }
    99     }
    100 
    101     // Fake reference to keep build scripts from removing ObjectFactory class. This class is not used directly but it's necessary for jaxb to work
    102     @SuppressWarnings("unused")
    103     private static final ObjectFactory OBJECT_FACTORY = null;
    104 
    105     // these values correspond to the zoom levels used throughout OSM and are in meters/pixel from zoom level 0 to 18.
    106     // taken from http://wiki.openstreetmap.org/wiki/Zoom_levels
    107     private static final Double[] snapLevels = { 156412.0, 78206.0, 39103.0, 19551.0, 9776.0, 4888.0,
    108         2444.0, 1222.0, 610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547, 4.773, 2.387, 1.193, 0.596 };
    109 
    110     public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true);
    111     public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
    112     public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false);
    113     public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14);
    114     public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4);
    115     public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 500);
     34public class WMSLayer extends AbstractTileSourceLayer {
     35    /** default tile size for WMS Layer */
     36    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 512);
     37    /** should WMS layer autozoom in default mode */
    11638    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true);
    11739
    118     public int messageNum = 5; //limit for messages per layer
    119     protected double resolution;
    120     protected String resolutionText;
    121     protected int imageSize;
    122     protected int dax = 10;
    123     protected int day = 10;
    124     protected int daStep = 5;
    125     protected int minZoom = 3;
    126 
    127     protected GeorefImage[][] images;
    128     protected static final int serializeFormatVersion = 5;
    129     protected boolean autoDownloadEnabled = true;
    130     protected boolean autoResolutionEnabled = PROP_DEFAULT_AUTOZOOM.get();
    131     protected boolean settingsChanged;
    132     public transient WmsCache cache;
    133     private transient AttributionSupport attribution = new AttributionSupport();
    134 
    135     // Image index boundary for current view
    136     private volatile int bminx;
    137     private volatile int bminy;
    138     private volatile int bmaxx;
    139     private volatile int bmaxy;
    140     private volatile int leftEdge;
    141     private volatile int bottomEdge;
    142 
    143     // Request queue
    144     private final transient List<WMSRequest> requestQueue = new ArrayList<>();
    145     private final transient List<WMSRequest> finishedRequests = new ArrayList<>();
    146     /**
    147      * List of request currently being processed by download threads
    148      */
    149     private final transient List<WMSRequest> processingRequests = new ArrayList<>();
    150     private final transient Lock requestQueueLock = new ReentrantLock();
    151     private final transient Condition queueEmpty = requestQueueLock.newCondition();
    152     private final transient List<WMSGrabber> grabbers = new ArrayList<>();
    153     private final transient List<Thread> grabberThreads = new ArrayList<>();
    154     private boolean canceled;
    155 
    156     /** set to true if this layer uses an invalid base url */
    157     private boolean usesInvalidUrl = false;
    158     /** set to true if the user confirmed to use an potentially invalid WMS base url */
    159     private boolean isInvalidUrlConfirmed = false;
    160 
    161     /**
    162      * Constructs a new {@code WMSLayer}.
    163      */
    164     public WMSLayer() {
    165         this(new ImageryInfo(tr("Blank Layer")));
    166     }
    167 
    16840    /**
    16941     * Constructs a new {@code WMSLayer}.
     42     * @param info ImageryInfo description of the layer
    17043     */
    17144    public WMSLayer(ImageryInfo info) {
    17245        super(info);
    173         imageSize = PROP_IMAGE_SIZE.get();
    174         setBackgroundLayer(true); /* set global background variable */
    175         initializeImages();
    176 
    177         attribution.initialize(this.info);
    178 
    179         Main.pref.addPreferenceChangeListener(this);
    180     }
    181 
    182     @Override
    183     public void hookUpMapView() {
    184         if (info.getUrl() != null) {
    185             startGrabberThreads();
    186 
    187             for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {
    188                 if (layer.getInfo().getUrl().equals(info.getUrl())) {
    189                     cache = layer.cache;
    190                     break;
    191                 }
    192             }
    193             if (cache == null) {
    194                 cache = new WmsCache(info.getUrl(), imageSize);
    195                 cache.loadIndex();
    196             }
    197         }
    198 
    199         // if automatic resolution is enabled, ensure that the first zoom level
    200         // is already snapped. Otherwise it may load tiles that will never get
    201         // used again when zooming.
    202         updateResolutionSetting(this, autoResolutionEnabled);
    203 
    204         final MouseAdapter adapter = new MouseAdapter() {
    205             @Override
    206             public void mouseClicked(MouseEvent e) {
    207                 if (!isVisible()) return;
    208                 if (e.getButton() == MouseEvent.BUTTON1) {
    209                     attribution.handleAttribution(e.getPoint(), true);
    210                 }
    211             }
    212         };
    213         Main.map.mapView.addMouseListener(adapter);
    214 
    215         MapView.addLayerChangeListener(new LayerChangeListener() {
    216             @Override
    217             public void activeLayerChange(Layer oldLayer, Layer newLayer) {
    218                 //
    219             }
    220 
    221             @Override
    222             public void layerAdded(Layer newLayer) {
    223                 //
    224             }
    225 
    226             @Override
    227             public void layerRemoved(Layer oldLayer) {
    228                 if (oldLayer == WMSLayer.this) {
    229                     Main.map.mapView.removeMouseListener(adapter);
    230                     MapView.removeLayerChangeListener(this);
    231                 }
    232             }
    233         });
    234     }
    235 
    236     public void doSetName(String name) {
    237         setName(name);
    238         info.setName(name);
    239     }
    240 
    241     public boolean hasAutoDownload(){
    242         return autoDownloadEnabled;
    243     }
    244 
    245     public void setAutoDownload(boolean val) {
    246         autoDownloadEnabled = val;
    247     }
    248 
    249     public boolean isAutoResolution() {
    250         return autoResolutionEnabled;
    251     }
    252 
    253     public void setAutoResolution(boolean val) {
    254         autoResolutionEnabled = val;
    255     }
    256 
    257     public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
    258         Set<Point> requestedTiles = new HashSet<>();
    259         for (LatLon point: points) {
    260             EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));
    261             EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));
    262             int minX = getImageXIndex(minEn.east());
    263             int maxX = getImageXIndex(maxEn.east());
    264             int minY = getImageYIndex(minEn.north());
    265             int maxY = getImageYIndex(maxEn.north());
    266 
    267             for (int x=minX; x<=maxX; x++) {
    268                 for (int y=minY; y<=maxY; y++) {
    269                     requestedTiles.add(new Point(x, y));
    270                 }
    271             }
    272         }
    273 
    274         for (Point p: requestedTiles) {
    275             addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));
    276         }
    277 
    278         precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());
    279         precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));
    280     }
    281 
    282     @Override
    283     public void destroy() {
    284         super.destroy();
    285         cancelGrabberThreads(false);
    286         Main.pref.removePreferenceChangeListener(this);
    287         if (cache != null) {
    288             cache.saveIndex();
    289         }
    290     }
    291 
    292     public final void initializeImages() {
    293         GeorefImage[][] old = images;
    294         images = new GeorefImage[dax][day];
    295         if (old != null) {
    296             for (GeorefImage[] row : old) {
    297                 for (GeorefImage image : row) {
    298                     images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;
    299                 }
    300             }
    301         }
    302         for(int x = 0; x<dax; ++x) {
    303             for(int y = 0; y<day; ++y) {
    304                 if (images[x][y] == null) {
    305                     images[x][y]= new GeorefImage(this);
    306                 }
    307             }
    308         }
    309     }
    310 
    311     @Override public ImageryInfo getInfo() {
    312         return info;
    313     }
    314 
    315     @Override public String getToolTipText() {
    316         if(autoDownloadEnabled)
    317             return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);
    318         else
    319             return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);
    320     }
    321 
    322     private int modulo(int a, int b) {
    323         return a % b >= 0 ? a%b : a%b+b;
    324     }
    325 
    326     private boolean zoomIsTooBig() {
    327         //don't download when it's too outzoomed
    328         return info.getPixelPerDegree() / getPPD() > minZoom;
    32946    }
    33047
    33148    @Override
    332     public void paint(Graphics2D g, final MapView mv, Bounds b) {
    333         if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
    334 
    335         if (autoResolutionEnabled && !Utils.equalsEpsilon(getBestZoom(), mv.getDist100Pixel())) {
    336             changeResolution(this, true);
    337         }
    338 
    339         settingsChanged = false;
    340 
    341         ProjectionBounds bounds = mv.getProjectionBounds();
    342         bminx= getImageXIndex(bounds.minEast);
    343         bminy= getImageYIndex(bounds.minNorth);
    344         bmaxx= getImageXIndex(bounds.maxEast);
    345         bmaxy= getImageYIndex(bounds.maxNorth);
    346 
    347         leftEdge = (int)(bounds.minEast * getPPD());
    348         bottomEdge = (int)(bounds.minNorth * getPPD());
    349 
    350         if (zoomIsTooBig()) {
    351             for(int x = 0; x<images.length; ++x) {
    352                 for(int y = 0; y<images[0].length; ++y) {
    353                     GeorefImage image = images[x][y];
    354                     image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
    355                 }
    356             }
    357         } else {
    358             downloadAndPaintVisible(g, mv, false);
     49    public Action[] getMenuEntries() {
     50        List<Action> ret = new ArrayList<>();
     51        ret.addAll(Arrays.asList(super.getMenuEntries()));
     52        ret.add(SeparatorLayerAction.INSTANCE);
     53        ret.add(new LayerSaveAction(this));
     54        ret.add(new LayerSaveAsAction(this));
     55        ret.add(new BookmarkWmsAction());
     56        return ret.toArray(new Action[]{});
    35957    }
    36058
    361         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
    362     }
    36359
    36460    @Override
    365     public void setOffset(double dx, double dy) {
    366         super.setOffset(dx, dy);
    367         settingsChanged = true;
    368     }
    369 
    370     public int getImageXIndex(double coord) {
    371         return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
    372     }
    373 
    374     public int getImageYIndex(double coord) {
    375         return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
    376     }
    377 
    378     public int getImageX(int imageIndex) {
    379         return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
    380     }
    381 
    382     public int getImageY(int imageIndex) {
    383         return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
    384     }
    385 
    386     public int getImageWidth(int xIndex) {
    387         return getImageX(xIndex + 1) - getImageX(xIndex);
    388     }
    389 
    390     public int getImageHeight(int yIndex) {
    391         return getImageY(yIndex + 1) - getImageY(yIndex);
    392     }
    393 
    394     /**
    395      *
    396      * @return Size of image in original zoom
    397      */
    398     public int getBaseImageWidth() {
    399         int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_EAST.get() * imageSize / 100 : 0;
    400         return imageSize + overlap;
    401     }
    402 
    403     /**
    404      *
    405      * @return Size of image in original zoom
    406      */
    407     public int getBaseImageHeight() {
    408         int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_NORTH.get() * imageSize / 100 : 0;
    409         return imageSize + overlap;
    410     }
    411 
    412     public int getImageSize() {
    413         return imageSize;
    414     }
    415 
    416     public boolean isOverlapEnabled() {
    417         return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0);
    418     }
    419 
    420     /**
    421      *
    422      * @return When overlapping is enabled, return visible part of tile. Otherwise return original image
    423      */
    424     public BufferedImage normalizeImage(BufferedImage img) {
    425         if (isOverlapEnabled()) {
    426             BufferedImage copy = img;
    427             img = new BufferedImage(imageSize, imageSize, copy.getType());
    428             img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize,
    429                     0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null);
    430         }
    431         return img;
    432     }
    433 
    434     /**
    435      *
    436      * @param xIndex
    437      * @param yIndex
    438      * @return Real EastNorth of given tile. dx/dy is not counted in
    439      */
    440     public EastNorth getEastNorth(int xIndex, int yIndex) {
    441         return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
    442     }
    443 
    444     protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
    445 
    446         int newDax = dax;
    447         int newDay = day;
    448 
    449         if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
    450             newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
    451         }
    452 
    453         if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
    454             newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
    455         }
    456 
    457         if (newDax != dax || newDay != day) {
    458             dax = newDax;
    459             day = newDay;
    460             initializeImages();
    461         }
    462 
    463         for(int x = bminx; x<=bmaxx; ++x) {
    464             for(int y = bminy; y<=bmaxy; ++y){
    465                 images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
    466             }
    467         }
    468 
    469         gatherFinishedRequests();
    470         Set<ProjectionBounds> areaToCache = new HashSet<>();
    471 
    472         for(int x = bminx; x<=bmaxx; ++x) {
    473             for(int y = bminy; y<=bmaxy; ++y){
    474                 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
    475                 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
    476                     addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, true));
    477                     areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
    478                 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
    479                     addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, false));
    480                     areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
    481                 }
    482             }
    483         }
    484         if (cache != null) {
    485             cache.setAreaToCache(areaToCache);
    486         }
    487     }
    488 
    489     @Override public void visitBoundingBox(BoundingXYVisitor v) {
    490         for(int x = 0; x<dax; ++x) {
    491             for(int y = 0; y<day; ++y)
    492                 if(images[x][y].getImage() != null){
    493                     v.visit(images[x][y].getMin());
    494                     v.visit(images[x][y].getMax());
    495                 }
    496         }
    497     }
    498 
    499     @Override public Action[] getMenuEntries() {
    500         return new Action[]{
    501                 LayerListDialog.getInstance().createActivateLayerAction(this),
    502                 LayerListDialog.getInstance().createShowHideLayerAction(),
    503                 LayerListDialog.getInstance().createDeleteLayerAction(),
    504                 SeparatorLayerAction.INSTANCE,
    505                 new OffsetAction(),
    506                 new LayerSaveAction(this),
    507                 new LayerSaveAsAction(this),
    508                 new BookmarkWmsAction(),
    509                 SeparatorLayerAction.INSTANCE,
    510                 new StartStopAction(),
    511                 new ToggleAlphaAction(),
    512                 new ToggleAutoResolutionAction(),
    513                 new ChangeResolutionAction(),
    514                 new ZoomToNativeResolution(),
    515                 new ReloadErrorTilesAction(),
    516                 new DownloadAction(),
    517                 SeparatorLayerAction.INSTANCE,
    518                 new LayerListPopup.InfoAction(this)
    519         };
     61    public TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
     62        if (info.getImageryType() == ImageryType.WMS && info.getUrl() != null) {
     63            TemplatedWMSTileSource.checkUrl(info.getUrl());
     64            TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info);
     65            info.setAttribution(tileSource);
     66            return tileSource;
    52067        }
    521 
    522     public GeorefImage findImage(EastNorth eastNorth) {
    523         int xIndex = getImageXIndex(eastNorth.east());
    524         int yIndex = getImageYIndex(eastNorth.north());
    525         GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
    526         if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
    527             return result;
    528         else
    52968        return null;
    53069    }
    53170
    53271    /**
    533      *
    534      * @param request
    535      * @return -1 if request is no longer needed, otherwise priority of request (lower number &lt;=&gt; more important request)
    536      */
    537     private int getRequestPriority(WMSRequest request) {
    538         if (!Utils.equalsEpsilon(request.getPixelPerDegree(), info.getPixelPerDegree()))
    539             return -1;
    540         if (bminx > request.getXIndex()
    541                 || bmaxx < request.getXIndex()
    542                 || bminy > request.getYIndex()
    543                 || bmaxy < request.getYIndex())
    544             return -1;
    545 
    546         MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
    547         EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
    548         int mouseX = getImageXIndex(cursorEastNorth.east());
    549         int mouseY = getImageYIndex(cursorEastNorth.north());
    550         int dx = request.getXIndex() - mouseX;
    551         int dy = request.getYIndex() - mouseY;
    552 
    553         return 1 + dx * dx + dy * dy;
    554     }
    555 
    556     private void sortRequests(boolean localOnly) {
    557         Iterator<WMSRequest> it = requestQueue.iterator();
    558         while (it.hasNext()) {
    559             WMSRequest item = it.next();
    560 
    561             if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
    562                 it.remove();
    563                 continue;
    564             }
    565 
    566             int priority = getRequestPriority(item);
    567             if (priority == -1 && item.isPrecacheOnly()) {
    568                 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
    569             }
    570 
    571             if (localOnly && !item.hasExactMatch()) {
    572                 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
    573             }
    574 
    575             if (       priority == -1
    576                     || finishedRequests.contains(item)
    577                     || processingRequests.contains(item)) {
    578                 it.remove();
    579             } else {
    580                 item.setPriority(priority);
    581             }
    582         }
    583         Collections.sort(requestQueue);
    584     }
    585 
    586     public WMSRequest getRequest(boolean localOnly) {
    587         requestQueueLock.lock();
    588         try {
    589             sortRequests(localOnly);
    590             while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
    591                 try {
    592                     queueEmpty.await();
    593                     sortRequests(localOnly);
    594                 } catch (InterruptedException e) {
    595                     Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
    596                 }
    597             }
    598 
    599             if (canceled)
    600                 return null;
    601             else {
    602                 WMSRequest request = requestQueue.remove(0);
    603                 processingRequests.add(request);
    604                 return request;
    605             }
    606 
    607         } finally {
    608             requestQueueLock.unlock();
    609         }
    610     }
    611 
    612     public void finishRequest(WMSRequest request) {
    613         requestQueueLock.lock();
    614         try {
    615             PrecacheTask task = request.getPrecacheTask();
    616             if (task != null) {
    617                 task.processedCount++;
    618                 if (!task.progressMonitor.isCanceled()) {
    619                     task.progressMonitor.worked(1);
    620                     task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
    621                 }
    622             }
    623             processingRequests.remove(request);
    624             if (request.getState() != null && !request.isPrecacheOnly()) {
    625                 finishedRequests.add(request);
    626                 if (Main.isDisplayingMapView()) {
    627                     Main.map.mapView.repaint();
    628                 }
    629             }
    630         } finally {
    631             requestQueueLock.unlock();
    632         }
    633     }
    634 
    635     public void addRequest(WMSRequest request) {
    636         requestQueueLock.lock();
    637         try {
    638 
    639             if (cache != null) {
    640                 ProjectionBounds b = getBounds(request);
    641                 // Checking for exact match is fast enough, no need to do it in separated thread
    642                 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
    643                 if (request.isPrecacheOnly() && request.hasExactMatch())
    644                     return; // We already have this tile cached
    645             }
    646 
    647             if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
    648                 requestQueue.add(request);
    649                 if (request.getPrecacheTask() != null) {
    650                     request.getPrecacheTask().totalCount++;
    651                 }
    652                 queueEmpty.signalAll();
    653             }
    654         } finally {
    655             requestQueueLock.unlock();
    656         }
    657     }
    658 
    659     public boolean requestIsVisible(WMSRequest request) {
    660         return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
    661     }
    662 
    663     private void gatherFinishedRequests() {
    664         requestQueueLock.lock();
    665         try {
    666             for (WMSRequest request: finishedRequests) {
    667                 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
    668                 if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
    669                     WMSException we = request.getException();
    670                     img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null);
    671                 }
    672             }
    673         } finally {
    674             requestQueueLock.unlock();
    675             finishedRequests.clear();
    676         }
    677     }
    678 
    679     public class DownloadAction extends AbstractAction {
    680         /**
    681          * Constructs a new {@code DownloadAction}.
    682          */
    683         public DownloadAction() {
    684             super(tr("Download visible tiles"));
    685         }
    686         @Override
    687         public void actionPerformed(ActionEvent ev) {
    688             if (zoomIsTooBig()) {
    689                 JOptionPane.showMessageDialog(
    690                         Main.parent,
    691                         tr("The requested area is too big. Please zoom in a little, or change resolution"),
    692                         tr("Error"),
    693                         JOptionPane.ERROR_MESSAGE
    694                         );
    695             } else {
    696                 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
    697             }
    698         }
    699     }
    700 
    701     /**
    702      * Finds the most suitable resolution for the current zoom level, but prefers
    703      * higher resolutions. Snaps to values defined in snapLevels.
    704      * @return best zoom level
    705      */
    706     private static double getBestZoom() {
    707         // not sure why getDist100Pixel returns values corresponding to
    708         // the snapLevels, which are in meters per pixel. It works, though.
    709         double dist = Main.map.mapView.getDist100Pixel();
    710         for(int i = snapLevels.length-2; i >= 0; i--) {
    711             if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
    712                 return snapLevels[i+1];
    713         }
    714         return snapLevels[0];
    715     }
    716 
    717     /**
    718      * Updates the given layer’s resolution settings to the current zoom level. Does
    719      * not update existing tiles, only new ones will be subject to the new settings.
    720      *
    721      * @param layer
    722      * @param snap  Set to true if the resolution should snap to certain values instead of
    723      *              matching the current zoom level perfectly
    724      */
    725     private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
    726         if(snap) {
    727             layer.resolution = getBestZoom();
    728             layer.resolutionText = MapView.getDistText(layer.resolution);
    729         } else {
    730             layer.resolution = Main.map.mapView.getDist100Pixel();
    731             layer.resolutionText = Main.map.mapView.getDist100PixelText();
    732         }
    733         layer.info.setPixelPerDegree(layer.getPPD());
    734     }
    735 
    736     /**
    737      * Updates the given layer’s resolution settings to the current zoom level and
    738      * updates existing tiles. If round is true, tiles will be updated gradually, if
    739      * false they will be removed instantly (and redrawn only after the new resolution
    740      * image has been loaded).
    741      * @param layer
    742      * @param snap  Set to true if the resolution should snap to certain values instead of
    743      *              matching the current zoom level perfectly
    744      */
    745     private static void changeResolution(WMSLayer layer, boolean snap) {
    746         updateResolutionSetting(layer, snap);
    747 
    748         layer.settingsChanged = true;
    749 
    750         // Don’t move tiles off screen when the resolution is rounded. This
    751         // prevents some flickering when zooming with auto-resolution enabled
    752         // and instead gradually updates each tile.
    753         if(!snap) {
    754             for(int x = 0; x<layer.dax; ++x) {
    755                 for(int y = 0; y<layer.day; ++y) {
    756                     layer.images[x][y].changePosition(-1, -1);
    757                 }
    758             }
    759         }
    760     }
    761 
    762     public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
    763 
    764         /**
    765          * Constructs a new {@code ChangeResolutionAction}
    766          */
    767         public ChangeResolutionAction() {
    768             super(tr("Change resolution"));
    769         }
    770 
    771         @Override
    772         public void actionPerformed(ActionEvent ev) {
    773             List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
    774             for (Layer l: layers) {
    775                 changeResolution((WMSLayer) l, false);
    776             }
    777             Main.map.mapView.repaint();
    778         }
    779 
    780         @Override
    781         public boolean supportLayers(List<Layer> layers) {
    782             for (Layer l: layers) {
    783                 if (!(l instanceof WMSLayer))
    784                     return false;
    785             }
    786             return true;
    787         }
    788 
    789         @Override
    790         public Component createMenuComponent() {
    791             return new JMenuItem(this);
    792         }
    793     }
    794 
    795     public class ReloadErrorTilesAction extends AbstractAction {
    796         /**
    797          * Constructs a new {@code ReloadErrorTilesAction}.
    798          */
    799         public ReloadErrorTilesAction() {
    800             super(tr("Reload erroneous tiles"));
    801         }
    802         @Override
    803         public void actionPerformed(ActionEvent ev) {
    804             // Delete small files, because they're probably blank tiles.
    805             // See #2307
    806             cache.cleanSmallFiles(4096);
    807 
    808             for (int x = 0; x < dax; ++x) {
    809                 for (int y = 0; y < day; ++y) {
    810                     GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
    811                     if(img.getState() == State.FAILED){
    812                         addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
    813                     }
    814                 }
    815             }
    816         }
    817     }
    818 
    819     public class ToggleAlphaAction extends AbstractAction implements LayerAction {
    820         /**
    821          * Constructs a new {@code ToggleAlphaAction}.
    822          */
    823         public ToggleAlphaAction() {
    824             super(tr("Alpha channel"));
    825         }
    826         @Override
    827         public void actionPerformed(ActionEvent ev) {
    828             JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
    829             boolean alphaChannel = checkbox.isSelected();
    830             PROP_ALPHA_CHANNEL.put(alphaChannel);
    831             Main.info("WMS Alpha channel changed to "+alphaChannel);
    832 
    833             // clear all resized cached instances and repaint the layer
    834             for (int x = 0; x < dax; ++x) {
    835                 for (int y = 0; y < day; ++y) {
    836                     GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
    837                     img.flushResizedCachedInstance();
    838                     BufferedImage bi = img.getImage();
    839                     // Completely erases images for which transparency has been forced,
    840                     // or images that should be forced now, as they need to be recreated
    841                     if (ImageProvider.isTransparencyForced(bi) || ImageProvider.hasTransparentColor(bi)) {
    842                         img.resetImage();
    843                     }
    844                 }
    845             }
    846             Main.map.mapView.repaint();
    847         }
    848 
    849         @Override
    850         public Component createMenuComponent() {
    851             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    852             item.setSelected(PROP_ALPHA_CHANNEL.get());
    853             return item;
    854         }
    855 
    856         @Override
    857         public boolean supportLayers(List<Layer> layers) {
    858             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    859         }
    860     }
    861 
    862     public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
    863 
    864         /**
    865          * Constructs a new {@code ToggleAutoResolutionAction}.
    866          */
    867         public ToggleAutoResolutionAction() {
    868             super(tr("Automatically change resolution"));
    869         }
    870 
    871         @Override
    872         public void actionPerformed(ActionEvent ev) {
    873             JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
    874             autoResolutionEnabled = checkbox.isSelected();
    875         }
    876 
    877         @Override
    878         public Component createMenuComponent() {
    879             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    880             item.setSelected(autoResolutionEnabled);
    881             return item;
    882         }
    883 
    884         @Override
    885         public boolean supportLayers(List<Layer> layers) {
    886             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    887         }
    888     }
    889 
    890     /**
    89172     * This action will add a WMS layer menu entry with the current WMS layer
    89273     * URL and name extended by the current resolution.
    89374     * When using the menu entry again, the WMS cache will be used properly.
    public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC  
    90586        }
    90687    }
    90788
    908     private class StartStopAction extends AbstractAction implements LayerAction {
    909 
    910         public StartStopAction() {
    911             super(tr("Automatic downloading"));
    912         }
    913 
    914         @Override
    915         public Component createMenuComponent() {
    916             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    917             item.setSelected(autoDownloadEnabled);
    918             return item;
    919         }
    920 
    921         @Override
    922         public boolean supportLayers(List<Layer> layers) {
    923             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    924         }
    925 
    926         @Override
    927         public void actionPerformed(ActionEvent e) {
    928             autoDownloadEnabled = !autoDownloadEnabled;
    929             if (autoDownloadEnabled) {
    930                 for (int x = 0; x < dax; ++x) {
    931                     for (int y = 0; y < day; ++y) {
    932                         GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
    933                         if(img.getState() == State.NOT_IN_CACHE){
    934                             addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
    935                         }
    936                     }
    937                 }
    938                 Main.map.mapView.repaint();
    939             }
    940         }
    941     }
    942 
    943     private class ZoomToNativeResolution extends AbstractAction {
    944 
    945         public ZoomToNativeResolution() {
    946             super(tr("Zoom to native resolution"));
    947         }
    948 
    949         @Override
    950         public void actionPerformed(ActionEvent e) {
    951             Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
    952         }
    953     }
    954 
    955     private void cancelGrabberThreads(boolean wait) {
    956         requestQueueLock.lock();
    957         try {
    958             canceled = true;
    959             for (WMSGrabber grabber: grabbers) {
    960                 grabber.cancel();
    961             }
    962             queueEmpty.signalAll();
    963         } finally {
    964             requestQueueLock.unlock();
    965         }
    966         if (wait) {
    967             for (Thread t: grabberThreads) {
    968                 try {
    969                     t.join();
    970                 } catch (InterruptedException e) {
    971                     Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");
    972                 }
    973             }
    974         }
    975     }
    976 
    977     private void startGrabberThreads() {
    978         int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
    979         requestQueueLock.lock();
    980         try {
    981             canceled = false;
    982             grabbers.clear();
    983             grabberThreads.clear();
    984             for (int i=0; i<threadCount; i++) {
    985                 WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);
    986                 grabbers.add(grabber);
    987                 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
    988                 t.setDaemon(true);
    989                 t.start();
    990                 grabberThreads.add(t);
    991             }
    992         } finally {
    993             requestQueueLock.unlock();
    994         }
    995     }
    996 
    997     @Override
    998     public boolean isChanged() {
    999         requestQueueLock.lock();
    1000         try {
    1001             return !finishedRequests.isEmpty() || settingsChanged;
    1002         } finally {
    1003             requestQueueLock.unlock();
    1004         }
    1005     }
    1006 
    1007     @Override
    1008     public void preferenceChanged(PreferenceChangeEvent event) {
    1009         if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {
    1010             cancelGrabberThreads(true);
    1011             startGrabberThreads();
    1012         } else if (
    1013                 event.getKey().equals(PROP_OVERLAP.getKey())
    1014                 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
    1015                 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
    1016             for (int i=0; i<images.length; i++) {
    1017                 for (int k=0; k<images[i].length; k++) {
    1018                     images[i][k] = new GeorefImage(this);
    1019                 }
    1020             }
    1021 
    1022             settingsChanged = true;
    1023         }
    1024     }
    102589
    102690    /**
    102791     * Checks that WMS layer is a grabber-compatible one (HTML or WMS).
    public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC  
    102993     * @since 8068
    103094     */
    103195    public void checkGrabberType() {
    1032         ImageryType it = getInfo().getImageryType();
    1033         if (!ImageryType.HTML.equals(it) && !ImageryType.WMS.equals(it))
    1034             throw new IllegalStateException("getGrabber() called for non-WMS layer type");
    1035     }
    1036 
    1037     protected WMSGrabber getGrabber(boolean localOnly) {
    1038         checkGrabberType();
    1039         if (getInfo().getImageryType() == ImageryType.HTML)
    1040             return new HTMLGrabber(Main.map.mapView, this, localOnly);
    1041         else
    1042             return new WMSGrabber(Main.map.mapView, this, localOnly);
    1043     }
    1044 
    1045     public ProjectionBounds getBounds(WMSRequest request) {
    1046         ProjectionBounds result = new ProjectionBounds(
    1047                 getEastNorth(request.getXIndex(), request.getYIndex()),
    1048                 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
    1049 
    1050         if (WMSLayer.PROP_OVERLAP.get()) {
    1051             double eastSize =  result.maxEast - result.minEast;
    1052             double northSize =  result.maxNorth - result.minNorth;
    1053 
    1054             double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
    1055             double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
    1056 
    1057             result = new ProjectionBounds(result.getMin(),
    1058                     new EastNorth(result.maxEast + eastCoef * eastSize,
    1059                             result.maxNorth + northCoef * northSize));
    1060         }
    1061         return result;
    106296    }
    106397
     98    private static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("WMS") {
    106499        @Override
    1065     public boolean isProjectionSupported(Projection proj) {
    1066         List<String> serverProjections = info.getServerProjections();
    1067         return serverProjections.contains(proj.toCode().toUpperCase(Locale.ENGLISH))
    1068                 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
    1069                 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
     100        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
     101                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
     102            return new WMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
    1070103        }
    1071104
    1072     @Override
    1073     public String nameSupportedProjections() {
    1074         StringBuilder res = new StringBuilder();
    1075         for (String p : info.getServerProjections()) {
    1076             if (res.length() > 0) {
    1077                 res.append(", ");
    1078             }
    1079             res.append(p);
    1080         }
    1081         return tr("Supported projections are: {0}", res);
    1082     }
    1083 
    1084     @Override
    1085     public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    1086         boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
    1087         Main.map.repaint(done ? 0 : 100);
    1088         return !done;
    1089     }
    1090 
    1091     @Override
    1092     public void writeExternal(ObjectOutput out) throws IOException {
    1093         out.writeInt(serializeFormatVersion);
    1094         out.writeInt(dax);
    1095         out.writeInt(day);
    1096         out.writeInt(imageSize);
    1097         out.writeDouble(info.getPixelPerDegree());
    1098         out.writeObject(info.getName());
    1099         out.writeObject(info.getExtendedUrl());
    1100         out.writeObject(images);
    1101     }
    1102 
    1103     @Override
    1104     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    1105         int sfv = in.readInt();
    1106         if (sfv != serializeFormatVersion)
    1107             throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
    1108         autoDownloadEnabled = false;
    1109         dax = in.readInt();
    1110         day = in.readInt();
    1111         imageSize = in.readInt();
    1112         info.setPixelPerDegree(in.readDouble());
    1113         doSetName((String)in.readObject());
    1114         info.setExtendedUrl((String)in.readObject());
    1115         images = (GeorefImage[][])in.readObject();
    1116 
    1117         for (GeorefImage[] imgs : images) {
    1118             for (GeorefImage img : imgs) {
    1119                 if (img != null) {
    1120                     img.setLayer(WMSLayer.this);
    1121                 }
    1122             }
    1123         }
    1124 
    1125         settingsChanged = true;
    1126         if (Main.isDisplayingMapView()) {
    1127             Main.map.mapView.repaint();
    1128         }
    1129         if (cache != null) {
    1130             cache.saveIndex();
    1131             cache = null;
    1132         }
    1133     }
     105    };
    1134106
    1135107    @Override
    1136     public void onPostLoadFromFile() {
    1137         if (info.getUrl() != null) {
    1138             cache = new WmsCache(info.getUrl(), imageSize);
    1139             startGrabberThreads();
    1140         }
     108    protected TileLoaderFactory getTileLoaderFactory() {
     109        return loaderFactory;
    1141110    }
    1142111
    1143112    @Override
    1144     public boolean isSavable() {
    1145         return true; // With WMSLayerExporter
     113    protected Map<String, String> getHeaders(TileSource tileSource) {
     114        if (tileSource instanceof TemplatedWMSTileSource) {
     115            return ((TemplatedWMSTileSource)tileSource).getHeaders();
    1146116        }
    1147 
    1148     @Override
    1149     public File createAndOpenSaveFileChooser() {
    1150         return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
     117        return null;
    1151118    }
    1152119}
  • src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java b/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java
    index ad63bbb..995cd17 100644
    a b import org.openstreetmap.josm.data.gpx.GpxTrackSegment;  
    2424import org.openstreetmap.josm.data.gpx.WayPoint;
    2525import org.openstreetmap.josm.gui.ExtendedDialog;
    2626import org.openstreetmap.josm.gui.PleaseWaitRunnable;
    27 import org.openstreetmap.josm.gui.layer.WMSLayer;
    28 import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
     27import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
     28import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer.PrecacheTask;
    2929import org.openstreetmap.josm.gui.progress.ProgressTaskId;
    3030import org.openstreetmap.josm.gui.progress.ProgressTaskIds;
    3131import org.openstreetmap.josm.gui.widgets.JosmComboBox;
    import org.openstreetmap.josm.tools.GBC;  
    3434import org.openstreetmap.josm.tools.ImageProvider;
    3535import org.xml.sax.SAXException;
    3636
     37/**
     38 * Class downloading WMS and TMS along the GPX track
     39 *
     40 */
    3741public class DownloadWmsAlongTrackAction extends AbstractAction {
    3842
    3943    private final transient GpxData data;
    4044
     45    /**
     46     * @param data that represents GPX track, along which data should be downloaded
     47     */
    4148    public DownloadWmsAlongTrackAction(final GpxData data) {
    4249        super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack"));
    4350        this.data = data;
    public class DownloadWmsAlongTrackAction extends AbstractAction {  
    5663        for (WayPoint p : data.waypoints) {
    5764            points.add(p.getCoor());
    5865        }
    59         final WMSLayer layer = askWMSLayer();
     66        final AbstractTileSourceLayer layer = askedLayer();
    6067        if (layer != null) {
    6168            PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) {
    6269                private PrecacheTask precacheTask;
    6370
    6471                @Override
    6572                protected void realRun() throws SAXException, IOException, OsmTransferException {
    66                     precacheTask = new PrecacheTask(progressMonitor);
     73                    precacheTask = layer.new PrecacheTask(progressMonitor);
    6774                    layer.downloadAreaToCache(precacheTask, points, 0, 0);
    6875                    while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) {
    6976                        synchronized (this) {
    public class DownloadWmsAlongTrackAction extends AbstractAction {  
    94101        }
    95102    }
    96103
    97     protected WMSLayer askWMSLayer() {
    98         Collection<WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class);
     104    protected AbstractTileSourceLayer askedLayer() {
     105        Collection<AbstractTileSourceLayer> targetLayers = Main.map.mapView.getLayersOfType(AbstractTileSourceLayer.class);
    99106        if (targetLayers.isEmpty()) {
    100107            warnNoImageryLayers();
    101108            return null;
    102109        }
    103         JosmComboBox<WMSLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new WMSLayer[0]));
     110        JosmComboBox<AbstractTileSourceLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new AbstractTileSourceLayer[0]));
    104111        layerList.setRenderer(new LayerListCellRenderer());
    105112        layerList.setSelectedIndex(0);
    106113        JPanel pnl = new JPanel(new GridBagLayout());
    public class DownloadWmsAlongTrackAction extends AbstractAction {  
    113120        if (ed.getValue() != 1) {
    114121            return null;
    115122        }
    116         return (WMSLayer) layerList.getSelectedItem();
     123        return (AbstractTileSourceLayer) layerList.getSelectedItem();
    117124    }
    118125
    119126    protected void warnNoImageryLayers() {
  • src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java

    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
    index 96c2c59..ed5168b 100644
    a b import javax.swing.JPanel;  
    1111import javax.swing.JSpinner;
    1212import javax.swing.SpinnerNumberModel;
    1313
     14import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
    1415import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    1516import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
    1617import org.openstreetmap.josm.gui.layer.TMSLayer;
    public class TMSSettingsPanel extends JPanel {  
    4041     */
    4142    public TMSSettingsPanel() {
    4243        super(new GridBagLayout());
    43         minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
    44         maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
     44        minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MIN_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
     45        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MAX_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
    4546        maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get().intValue(), 0, Integer.MAX_VALUE, 1));
    4647        maxConcurrentDownloads = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1));
    4748        maxDownloadsPerHost = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.HOST_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1));
    public class TMSSettingsPanel extends JPanel {  
    9495        this.addToSlippyMapChosser.setSelected(TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get());
    9596        this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null));
    9697        this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null));
    97         this.tilecacheDir.setText(TMSLayer.PROP_TILECACHE_DIR.get());
     98        this.tilecacheDir.setText(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get());
    9899        this.maxElementsOnDisk.setValue(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get());
    99100        this.maxConcurrentDownloads.setValue(TMSCachedTileLoaderJob.THREAD_LIMIT.get());
    100101        this.maxDownloadsPerHost.setValue(TMSCachedTileLoader.HOST_LIMIT.get());
    public class TMSSettingsPanel extends JPanel {  
    131132            restartRequired = true;
    132133        }
    133134
    134         if (!TMSLayer.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {
     135        if (!CachedTileLoaderFactory.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {
    135136            restartRequired = true;
    136             TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
     137            CachedTileLoaderFactory.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
    137138        }
    138139
    139140        return restartRequired;
  • src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java

    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java
    index 044c91b..dfe1e44 100644
    a b package org.openstreetmap.josm.gui.preferences.imagery;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.FlowLayout;
    76import java.awt.GridBagLayout;
    87
    98import javax.swing.Box;
    import javax.swing.JPanel;  
    1312import javax.swing.JSpinner;
    1413import javax.swing.SpinnerNumberModel;
    1514
     15import org.openstreetmap.josm.data.imagery.WMSCachedTileLoaderJob;
    1616import org.openstreetmap.josm.gui.layer.WMSLayer;
    17 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
    18 import org.openstreetmap.josm.io.imagery.HTMLGrabber;
    1917import org.openstreetmap.josm.tools.GBC;
    2018
    2119/**
    public class WMSSettingsPanel extends JPanel {  
    2624
    2725    // WMS Settings
    2826    private final JCheckBox autozoomActive;
    29     private final JosmComboBox<String> browser;
    30     private final JCheckBox overlapCheckBox;
    31     private final JSpinner spinEast;
    32     private final JSpinner spinNorth;
    3327    private final JSpinner spinSimConn;
     28    private final JSpinner tileSize;
    3429
    3530    /**
    3631     * Constructs a new {@code WMSSettingsPanel}.
    public class WMSSettingsPanel extends JPanel {  
    4439        add(GBC.glue(5, 0), GBC.std());
    4540        add(autozoomActive, GBC.eol().fill(GBC.HORIZONTAL));
    4641
    47         // Downloader
    48         browser = new JosmComboBox<>(new String[] {
    49                 "webkit-image {0}",
    50                 "gnome-web-photo --mode=photo --format=png {0} /dev/stdout",
    51                 "gnome-web-photo-fixed {0}",
    52         "webkit-image-gtk {0}"});
    53         browser.setEditable(true);
    54         add(new JLabel(tr("Downloader:")), GBC.std());
    55         add(GBC.glue(5, 0), GBC.std());
    56         add(browser, GBC.eol().fill(GBC.HORIZONTAL));
    57 
    5842        // Simultaneous connections
    5943        add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
    6044        JLabel labelSimConn = new JLabel(tr("Simultaneous connections:"));
    61         spinSimConn = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get().intValue(), 1, 30, 1));
     45        spinSimConn = new JSpinner(new SpinnerNumberModel(WMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 1, 30, 1));
     46        labelSimConn.setLabelFor(spinSimConn);
    6247        add(labelSimConn, GBC.std());
    6348        add(GBC.glue(5, 0), GBC.std());
    6449        add(spinSimConn, GBC.eol());
    6550
    66         // Overlap
    67         add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
    68 
    69         overlapCheckBox = new JCheckBox(tr("Overlap tiles"));
    70         JLabel labelEast = new JLabel(tr("% of east:"));
    71         JLabel labelNorth = new JLabel(tr("% of north:"));
    72         spinEast = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_EAST.get().intValue(), 1, 50, 1));
    73         spinNorth = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_NORTH.get().intValue(), 1, 50, 1));
    74 
    75         JPanel overlapPanel = new JPanel(new FlowLayout());
    76         overlapPanel.add(overlapCheckBox);
    77         overlapPanel.add(labelEast);
    78         overlapPanel.add(spinEast);
    79         overlapPanel.add(labelNorth);
    80         overlapPanel.add(spinNorth);
    81 
    82         add(overlapPanel, GBC.eop());
     51        // Tile size
     52        JLabel labelTileSize = new JLabel(tr("Tile size:"));
     53        tileSize = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_IMAGE_SIZE.get().intValue(), 1, 4096, 128));
     54        labelTileSize.setLabelFor(tileSize);
     55        add(labelTileSize, GBC.std());
     56        add(GBC.glue(5, 0), GBC.std());
     57        add(tileSize, GBC.eol());
    8358    }
    8459
    8560    /**
    public class WMSSettingsPanel extends JPanel {  
    8762     */
    8863    public void loadSettings() {
    8964        this.autozoomActive.setSelected(WMSLayer.PROP_DEFAULT_AUTOZOOM.get());
    90         this.browser.setSelectedItem(HTMLGrabber.PROP_BROWSER.get());
    91         this.overlapCheckBox.setSelected(WMSLayer.PROP_OVERLAP.get());
    92         this.spinEast.setValue(WMSLayer.PROP_OVERLAP_EAST.get());
    93         this.spinNorth.setValue(WMSLayer.PROP_OVERLAP_NORTH.get());
    94         this.spinSimConn.setValue(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get());
     65        this.spinSimConn.setValue(WMSCachedTileLoaderJob.THREAD_LIMIT.get());
     66        this.tileSize.setValue(WMSLayer.PROP_IMAGE_SIZE.get());
    9567    }
    9668
    9769    /**
    public class WMSSettingsPanel extends JPanel {  
    10072     */
    10173    public boolean saveSettings() {
    10274        WMSLayer.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected());
    103         WMSLayer.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
    104         WMSLayer.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
    105         WMSLayer.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
    106         WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
    107 
    108         HTMLGrabber.PROP_BROWSER.put(browser.getEditor().getItem().toString());
     75        WMSCachedTileLoaderJob.THREAD_LIMIT.put((Integer) spinSimConn.getModel().getValue());
     76        WMSLayer.PROP_IMAGE_SIZE.put((Integer) this.tileSize.getModel().getValue());
    10977
    11078        return false;
    11179    }
  • src/org/openstreetmap/josm/io/WMSLayerExporter.java

    diff --git a/src/org/openstreetmap/josm/io/WMSLayerExporter.java b/src/org/openstreetmap/josm/io/WMSLayerExporter.java
    index 4c8b1ed..15e765a 100644
    a b import java.io.FileOutputStream;  
    66import java.io.IOException;
    77import java.io.ObjectOutputStream;
    88
     9import org.openstreetmap.josm.Main;
     10import org.openstreetmap.josm.data.Preferences;
     11import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
     12import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
    913import org.openstreetmap.josm.gui.layer.Layer;
    10 import org.openstreetmap.josm.gui.layer.WMSLayer;
    1114import org.openstreetmap.josm.tools.CheckParameterUtil;
    1215
    1316/**
    import org.openstreetmap.josm.tools.CheckParameterUtil;  
    1720 */
    1821public class WMSLayerExporter extends FileExporter {
    1922
     23    /** Which version of the file we export */
     24    public static final int CURRENT_FILE_VERSION = 6;
     25
    2026    /**
    2127     * Constructs a new {@code WMSLayerExporter}
    2228     */
    public class WMSLayerExporter extends FileExporter {  
    2834    public void exportData(File file, Layer layer) throws IOException {
    2935        CheckParameterUtil.ensureParameterNotNull(file, "file");
    3036        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
    31         if (layer instanceof WMSLayer) {
     37
     38        if (layer instanceof AbstractTileSourceLayer) {
    3239            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
    33                 ((WMSLayer)layer).writeExternal(oos);
     40                oos.writeInt(CURRENT_FILE_VERSION); // file version
     41                oos.writeObject(Main.map.mapView.getCenter());
     42                ImageryPreferenceEntry entry = new ImageryPreferenceEntry(((AbstractTileSourceLayer) layer).getInfo());
     43                oos.writeObject(Preferences.serializeStruct(entry, ImageryPreferenceEntry.class));
    3444            }
    3545        }
     46
    3647    }
    3748
    3849    @Override
    3950    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
    40         setEnabled(newLayer instanceof WMSLayer);
     51        setEnabled(newLayer instanceof AbstractTileSourceLayer);
    4152    }
    4253}
  • src/org/openstreetmap/josm/io/WMSLayerImporter.java

    diff --git a/src/org/openstreetmap/josm/io/WMSLayerImporter.java b/src/org/openstreetmap/josm/io/WMSLayerImporter.java
    index ea1daa3..df06db0 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    66import java.io.File;
    77import java.io.FileInputStream;
    88import java.io.IOException;
     9import java.io.InvalidClassException;
    910import java.io.ObjectInputStream;
     11import java.util.Map;
    1012
    1113import org.openstreetmap.josm.Main;
    1214import org.openstreetmap.josm.actions.ExtensionFileFilter;
    13 import org.openstreetmap.josm.gui.layer.WMSLayer;
     15import org.openstreetmap.josm.data.Preferences;
     16import org.openstreetmap.josm.data.coor.EastNorth;
     17import org.openstreetmap.josm.data.imagery.ImageryInfo;
     18import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
     19import org.openstreetmap.josm.gui.layer.ImageryLayer;
    1420import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    1521import org.openstreetmap.josm.gui.util.GuiHelper;
    1622import org.openstreetmap.josm.tools.CheckParameterUtil;
    public class WMSLayerImporter extends FileImporter {  
    2733    public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter(
    2834            "wms", "wms", tr("WMS Files (*.wms)"));
    2935
    30     private final WMSLayer wmsLayer;
    31 
    3236    /**
    3337     * Constructs a new {@code WMSLayerImporter}.
    3438     */
    3539    public WMSLayerImporter() {
    36         this(new WMSLayer());
    37     }
    38 
    39     /**
    40      * Constructs a new {@code WMSLayerImporter} that will import data to the specified WMS layer.
    41      * @param wmsLayer The WMS layer.
    42      */
    43     public WMSLayerImporter(WMSLayer wmsLayer) {
    4440        super(FILE_FILTER);
    45         this.wmsLayer = wmsLayer;
    4641    }
    4742
     43
    4844    @Override
    4945    public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
    5046        CheckParameterUtil.ensureParameterNotNull(file, "file");
     47        final EastNorth zoomTo;
     48        ImageryInfo info = null;
     49        final ImageryLayer layer;
     50
    5151        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
    52             wmsLayer.readExternal(ois);
     52            int sfv = ois.readInt();
     53            if (sfv < 5) {
     54                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 5));
     55            } else if (sfv == 5) {
     56                ois.readInt(); // dax - not needed
     57                ois.readInt(); // day - not needed
     58                zoomTo = null;
     59
     60                int imageSize = ois.readInt();
     61                double pixelPerDegree = ois.readDouble();
     62
     63                String name = (String)ois.readObject();
     64                String extendedUrl = (String)ois.readObject();
     65
     66                info = new ImageryInfo(name);
     67                info.setExtendedUrl(extendedUrl);
     68                info.setPixelPerDegree(pixelPerDegree);
     69                info.setTileSize(imageSize);
     70            } else if (sfv == WMSLayerExporter.CURRENT_FILE_VERSION){
     71                zoomTo = (EastNorth) ois.readObject();
     72
     73                @SuppressWarnings("unchecked")
     74                ImageryPreferenceEntry entry = Preferences.deserializeStruct(
     75                        (Map<String, String>)ois.readObject(),
     76                        ImageryPreferenceEntry.class);
     77                info = new ImageryInfo(entry);
     78            } else {
     79                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 6));
     80            }
    5381        } catch (ClassNotFoundException e) {
    5482            throw new IllegalDataException(e);
    5583        }
     84        layer = ImageryLayer.create(info);
     85
    5686
    5787        // FIXME: remove UI stuff from IO subsystem
    5888        GuiHelper.runInEDT(new Runnable() {
    5989            @Override
    6090            public void run() {
    61                 Main.main.addLayer(wmsLayer);
    62                 wmsLayer.onPostLoadFromFile();
     91                Main.main.addLayer(layer);
     92                if (zoomTo != null) {
     93                    Main.map.mapView.zoomTo(zoomTo);
    6394                }
    64         });
    6595            }
    66 
    67     /**
    68      * Replies the imported WMS layer.
    69      * @return The imported WMS layer.
    70      * @see #importData(File, ProgressMonitor)
    71      */
    72     public final WMSLayer getWmsLayer() {
    73         return wmsLayer;
     96        });
    7497    }
    7598}
  • deleted file src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java

    diff --git a/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java b/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
    deleted file mode 100644
    index 591e8ef..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.io.imagery;
    3 
    4 import java.awt.image.BufferedImage;
    5 import java.io.ByteArrayInputStream;
    6 import java.io.ByteArrayOutputStream;
    7 import java.io.IOException;
    8 import java.net.URL;
    9 import java.text.MessageFormat;
    10 import java.util.ArrayList;
    11 import java.util.List;
    12 import java.util.StringTokenizer;
    13 
    14 import org.openstreetmap.josm.Main;
    15 import org.openstreetmap.josm.data.preferences.StringProperty;
    16 import org.openstreetmap.josm.gui.MapView;
    17 import org.openstreetmap.josm.gui.layer.WMSLayer;
    18 import org.openstreetmap.josm.tools.ImageProvider;
    19 import org.openstreetmap.josm.tools.Utils;
    20 
    21 public class HTMLGrabber extends WMSGrabber {
    22     public static final StringProperty PROP_BROWSER = new StringProperty("imagery.wms.browser", "webkit-image {0}");
    23 
    24     public HTMLGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
    25         super(mv, layer, localOnly);
    26     }
    27 
    28     @Override
    29     protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException {
    30         String urlstring = url.toExternalForm();
    31 
    32         Main.info("Grabbing HTML " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
    33 
    34         List<String> cmdParams = new ArrayList<>();
    35         StringTokenizer st = new StringTokenizer(MessageFormat.format(PROP_BROWSER.get(), urlstring));
    36         while (st.hasMoreTokens()) {
    37             cmdParams.add(st.nextToken());
    38         }
    39 
    40         ProcessBuilder builder = new ProcessBuilder( cmdParams);
    41 
    42         Process browser;
    43         try {
    44             browser = builder.start();
    45         } catch (IOException ioe) {
    46             throw new IOException("Could not start browser. Please check that the executable path is correct.\n" + ioe.getMessage(), ioe);
    47         }
    48 
    49         ByteArrayOutputStream baos = new ByteArrayOutputStream();
    50         Utils.copyStream(browser.getInputStream(), baos);
    51         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    52         BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));
    53         bais.reset();
    54         layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
    55 
    56         return img;
    57     }
    58 }
  • deleted file src/org/openstreetmap/josm/io/imagery/WMSException.java

    diff --git a/src/org/openstreetmap/josm/io/imagery/WMSException.java b/src/org/openstreetmap/josm/io/imagery/WMSException.java
    deleted file mode 100644
    index 9018b12..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.io.imagery;
    3 
    4 import java.net.URL;
    5 import java.util.Arrays;
    6 import java.util.Collection;
    7 
    8 import org.openstreetmap.josm.tools.Utils;
    9 
    10 /**
    11  * WMS Service Exception, as defined by {@code application/vnd.ogc.se_xml} format:<ul>
    12  * <li><a href="http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">WMS 1.1.0 DTD</a></li>
    13  * <li><a href="http://schemas.opengis.net/wms/1.3.0/exception_1_3_0.dtd">WMS 1.3.0 XSD</a></li>
    14  * </ul>
    15  * @since 7425
    16  */
    17 public class WMSException extends Exception {
    18 
    19     private final transient WMSRequest request;
    20     private final URL url;
    21     private final String[] exceptions;
    22 
    23     /**
    24      * Constructs a new {@code WMSException}.
    25      * @param request the WMS request that lead to this exception
    26      * @param url the URL that lead to this exception
    27      * @param exceptions the exceptions replied by WMS server
    28      */
    29     public WMSException(WMSRequest request, URL url, Collection<String> exceptions) {
    30         super(Utils.join("\n", exceptions));
    31         this.request = request;
    32         this.url = url;
    33         this.exceptions = exceptions.toArray(new String[0]);
    34     }
    35 
    36     /**
    37      * Replies the WMS request that lead to this exception.
    38      * @return the WMS request
    39      */
    40     public final WMSRequest getRequest() {
    41         return request;
    42     }
    43 
    44     /**
    45      * Replies the URL that lead to this exception.
    46      * @return the URL
    47      */
    48     public final URL getUrl() {
    49         return url;
    50     }
    51 
    52     /**
    53      * Replies the WMS Service exceptions.
    54      * @return the exceptions
    55      */
    56     public final Collection<String> getExceptions() {
    57         return Arrays.asList(exceptions);
    58     }
    59 }
  • deleted file src/org/openstreetmap/josm/io/imagery/WMSGrabber.java

    diff --git a/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java b/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
    deleted file mode 100644
    index f35b0c9..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.io.imagery;
    3 
    4 import java.awt.image.BufferedImage;
    5 import java.io.BufferedReader;
    6 import java.io.ByteArrayInputStream;
    7 import java.io.ByteArrayOutputStream;
    8 import java.io.IOException;
    9 import java.io.InputStream;
    10 import java.io.InputStreamReader;
    11 import java.io.StringReader;
    12 import java.net.HttpURLConnection;
    13 import java.net.MalformedURLException;
    14 import java.net.URL;
    15 import java.net.URLConnection;
    16 import java.nio.charset.StandardCharsets;
    17 import java.text.DecimalFormat;
    18 import java.text.DecimalFormatSymbols;
    19 import java.text.NumberFormat;
    20 import java.util.ArrayList;
    21 import java.util.HashMap;
    22 import java.util.List;
    23 import java.util.Locale;
    24 import java.util.Map;
    25 import java.util.Map.Entry;
    26 import java.util.regex.Matcher;
    27 import java.util.regex.Pattern;
    28 
    29 import javax.xml.parsers.DocumentBuilder;
    30 import javax.xml.parsers.DocumentBuilderFactory;
    31 import javax.xml.parsers.ParserConfigurationException;
    32 
    33 import org.openstreetmap.josm.Main;
    34 import org.openstreetmap.josm.data.ProjectionBounds;
    35 import org.openstreetmap.josm.data.coor.EastNorth;
    36 import org.openstreetmap.josm.data.coor.LatLon;
    37 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
    38 import org.openstreetmap.josm.data.imagery.ImageryInfo;
    39 import org.openstreetmap.josm.gui.MapView;
    40 import org.openstreetmap.josm.gui.layer.WMSLayer;
    41 import org.openstreetmap.josm.io.OsmTransferException;
    42 import org.openstreetmap.josm.io.ProgressInputStream;
    43 import org.openstreetmap.josm.tools.ImageProvider;
    44 import org.openstreetmap.josm.tools.Utils;
    45 import org.w3c.dom.Document;
    46 import org.w3c.dom.NodeList;
    47 import org.xml.sax.InputSource;
    48 import org.xml.sax.SAXException;
    49 
    50 /**
    51  * WMS grabber, fetching tiles from WMS server.
    52  * @since 3715
    53  */
    54 public class WMSGrabber implements Runnable {
    55 
    56     protected final MapView mv;
    57     protected final WMSLayer layer;
    58     private final boolean localOnly;
    59 
    60     protected ProjectionBounds b;
    61     protected volatile boolean canceled;
    62 
    63     protected String baseURL;
    64     private ImageryInfo info;
    65     private Map<String, String> props = new HashMap<>();
    66 
    67     /**
    68      * Constructs a new {@code WMSGrabber}.
    69      * @param mv Map view
    70      * @param layer WMS layer
    71      */
    72     public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
    73         this.mv = mv;
    74         this.layer = layer;
    75         this.localOnly = localOnly;
    76         this.info = layer.getInfo();
    77         this.baseURL = info.getUrl();
    78         if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {
    79             props.put("Cookie", layer.getInfo().getCookies());
    80         }
    81         Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
    82         StringBuffer output = new StringBuffer();
    83         Matcher matcher = pattern.matcher(this.baseURL);
    84         while (matcher.find()) {
    85             props.put(matcher.group(1),matcher.group(2));
    86             matcher.appendReplacement(output, "");
    87         }
    88         matcher.appendTail(output);
    89         this.baseURL = output.toString();
    90     }
    91 
    92     int width() {
    93         return layer.getBaseImageWidth();
    94     }
    95 
    96     int height() {
    97         return layer.getBaseImageHeight();
    98     }
    99 
    100     @Override
    101     public void run() {
    102         while (true) {
    103             if (canceled)
    104                 return;
    105             WMSRequest request = layer.getRequest(localOnly);
    106             if (request == null)
    107                 return;
    108             this.b = layer.getBounds(request);
    109             if (request.isPrecacheOnly()) {
    110                 if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {
    111                     attempt(request);
    112                 } else if (Main.isDebugEnabled()) {
    113                     Main.debug("Ignoring "+request+" (precache only + exact match)");
    114                 }
    115             } else if (!loadFromCache(request)){
    116                 attempt(request);
    117             } else if (Main.isDebugEnabled()) {
    118                 Main.debug("Ignoring "+request+" (loaded from cache)");
    119             }
    120             layer.finishRequest(request);
    121         }
    122     }
    123 
    124     protected void attempt(WMSRequest request){ // try to fetch the image
    125         int maxTries = 5; // n tries for every image
    126         for (int i = 1; i <= maxTries; i++) {
    127             if (canceled)
    128                 return;
    129             try {
    130                 if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))
    131                     return;
    132                 fetch(request, i);
    133                 break; // break out of the retry loop
    134             } catch (IOException e) {
    135                 try { // sleep some time and then ask the server again
    136                     Thread.sleep(random(1000, 2000));
    137                 } catch (InterruptedException e1) {
    138                     Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
    139                 }
    140                 if (i == maxTries) {
    141                     Main.error(e);
    142                     request.finish(State.FAILED, null, null);
    143                 }
    144             } catch (WMSException e) {
    145                 // Fail fast in case of WMS Service exception: useless to retry:
    146                 // either the URL is wrong or the server suffers huge problems
    147                 Main.error("WMS service exception while requesting "+e.getUrl()+":\n"+e.getMessage().trim());
    148                 request.finish(State.FAILED, null, e);
    149                 break; // break out of the retry loop
    150             }
    151         }
    152     }
    153 
    154     public static int random(int min, int max) {
    155         return (int)(Math.random() * ((max+1)-min) ) + min;
    156     }
    157 
    158     public final void cancel() {
    159         canceled = true;
    160     }
    161 
    162     private void fetch(WMSRequest request, int attempt) throws IOException, WMSException {
    163         URL url = null;
    164         try {
    165             url = getURL(
    166                     b.minEast, b.minNorth,
    167                     b.maxEast, b.maxNorth,
    168                     width(), height());
    169             request.finish(State.IMAGE, grab(request, url, attempt), null);
    170 
    171         } catch (IOException | OsmTransferException e) {
    172             Main.error(e);
    173             throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);
    174         }
    175     }
    176 
    177     public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
    178 
    179     protected URL getURL(double w, double s,double e,double n,
    180             int wi, int ht) throws MalformedURLException {
    181         String myProj = Main.getProjection().toCode();
    182         if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) {
    183             LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
    184             LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
    185             myProj = "EPSG:4326";
    186             s = sw.lat();
    187             w = sw.lon();
    188             n = ne.lat();
    189             e = ne.lon();
    190         }
    191         if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) {
    192             myProj = "CRS:84";
    193         }
    194 
    195         // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
    196         //
    197         // Background:
    198         //
    199         // bbox=x_min,y_min,x_max,y_max
    200         //
    201         //      SRS=... is WMS 1.1.1
    202         //      CRS=... is WMS 1.3.0
    203         //
    204         // The difference:
    205         //      For SRS x is east-west and y is north-south
    206         //      For CRS x and y are as specified by the EPSG
    207         //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
    208         //          For most other EPSG code there seems to be no difference.
    209         // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326
    210         boolean switchLatLon = false;
    211         if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=epsg:4326")) {
    212             switchLatLon = true;
    213         } else if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=") && "EPSG:4326".equals(myProj)) {
    214             switchLatLon = true;
    215         }
    216         String bbox;
    217         if (switchLatLon) {
    218             bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));
    219         } else {
    220             bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));
    221         }
    222         return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj)
    223                 .replaceAll("\\{bbox\\}", bbox)
    224                 .replaceAll("\\{w\\}", latLonFormat.format(w))
    225                 .replaceAll("\\{s\\}", latLonFormat.format(s))
    226                 .replaceAll("\\{e\\}", latLonFormat.format(e))
    227                 .replaceAll("\\{n\\}", latLonFormat.format(n))
    228                 .replaceAll("\\{width\\}", String.valueOf(wi))
    229                 .replaceAll("\\{height\\}", String.valueOf(ht))
    230                 .replace(" ", "%20"));
    231     }
    232 
    233     public boolean loadFromCache(WMSRequest request) {
    234         BufferedImage cached = layer.cache.getExactMatch(
    235                 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
    236 
    237         if (cached != null) {
    238             request.finish(State.IMAGE, cached, null);
    239             return true;
    240         } else if (request.isAllowPartialCacheMatch()) {
    241             BufferedImage partialMatch = layer.cache.getPartialMatch(
    242                     Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
    243             if (partialMatch != null) {
    244                 request.finish(State.PARTLY_IN_CACHE, partialMatch, null);
    245                 return true;
    246             }
    247         }
    248 
    249         if (!request.isReal() && !layer.hasAutoDownload()){
    250             request.finish(State.NOT_IN_CACHE, null, null);
    251             return true;
    252         }
    253 
    254         return false;
    255     }
    256 
    257     protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException {
    258         Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
    259 
    260         HttpURLConnection conn = Utils.openHttpConnection(url);
    261         conn.setUseCaches(true);
    262         for (Entry<String, String> e : props.entrySet()) {
    263             conn.setRequestProperty(e.getKey(), e.getValue());
    264         }
    265         conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000);
    266         conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000);
    267 
    268         String contentType = conn.getHeaderField("Content-Type");
    269         if (conn.getResponseCode() != 200
    270                 || contentType != null && !contentType.startsWith("image") ) {
    271             String xml = readException(conn);
    272             try {
    273                 DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    274                 InputSource is = new InputSource(new StringReader(xml));
    275                 Document doc = db.parse(is);
    276                 NodeList nodes = doc.getElementsByTagName("ServiceException");
    277                 List<String> exceptions = new ArrayList<>(nodes.getLength());
    278                 for (int i = 0; i < nodes.getLength(); i++) {
    279                     exceptions.add(nodes.item(i).getTextContent());
    280                 }
    281                 throw new WMSException(request, url, exceptions);
    282             } catch (SAXException | ParserConfigurationException ex) {
    283                 throw new IOException(xml, ex);
    284             }
    285         }
    286 
    287         ByteArrayOutputStream baos = new ByteArrayOutputStream();
    288         try (InputStream is = new ProgressInputStream(conn, null)) {
    289             Utils.copyStream(is, baos);
    290         }
    291 
    292         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    293         BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));
    294         bais.reset();
    295         layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
    296         return img;
    297     }
    298 
    299     protected String readException(URLConnection conn) throws IOException {
    300         StringBuilder exception = new StringBuilder();
    301         InputStream in = conn.getInputStream();
    302         try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
    303             String line = null;
    304             while( (line = br.readLine()) != null) {
    305                 // filter non-ASCII characters and control characters
    306                 exception.append(line.replaceAll("[^\\p{Print}]", ""));
    307                 exception.append('\n');
    308             }
    309             return exception.toString();
    310         }
    311     }
    312 }
  • deleted file src/org/openstreetmap/josm/io/imagery/WMSRequest.java

    diff --git a/src/org/openstreetmap/josm/io/imagery/WMSRequest.java b/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
    deleted file mode 100644
    index db25eb1..0000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 package org.openstreetmap.josm.io.imagery;
    3 
    4 import java.awt.image.BufferedImage;
    5 
    6 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
    7 import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
    8 
    9 public class WMSRequest implements Comparable<WMSRequest> {
    10     private final int xIndex;
    11     private final int yIndex;
    12     private final double pixelPerDegree;
    13     private final boolean real; // Download even if autodownloading is disabled
    14     private final PrecacheTask precacheTask; // Download even when wms tile is not currently visible (precache)
    15     private final boolean allowPartialCacheMatch;
    16     private int priority;
    17     private boolean hasExactMatch;
    18     // Result
    19     private State state;
    20     private BufferedImage image;
    21     private WMSException exception;
    22 
    23     public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch) {
    24         this(xIndex, yIndex, pixelPerDegree, real, allowPartialCacheMatch, null);
    25     }
    26 
    27     public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch, PrecacheTask precacheTask) {
    28         this.xIndex = xIndex;
    29         this.yIndex = yIndex;
    30         this.pixelPerDegree = pixelPerDegree;
    31         this.real = real;
    32         this.precacheTask = precacheTask;
    33         this.allowPartialCacheMatch = allowPartialCacheMatch;
    34     }
    35 
    36     public void finish(State state, BufferedImage image, WMSException exception) {
    37         this.state = state;
    38         this.image = image;
    39         this.exception = exception;
    40     }
    41 
    42     public int getXIndex() {
    43         return xIndex;
    44     }
    45 
    46     public int getYIndex() {
    47         return yIndex;
    48     }
    49 
    50     public double getPixelPerDegree() {
    51         return pixelPerDegree;
    52     }
    53 
    54     @Override
    55     public int hashCode() {
    56         final int prime = 31;
    57         int result = 1;
    58         long temp;
    59         temp = Double.doubleToLongBits(pixelPerDegree);
    60         result = prime * result + (int) (temp ^ (temp >>> 32));
    61         result = prime * result + xIndex;
    62         result = prime * result + yIndex;
    63         return result;
    64     }
    65 
    66     @Override
    67     public boolean equals(Object obj) {
    68         if (this == obj)
    69             return true;
    70         if (obj == null)
    71             return false;
    72         if (getClass() != obj.getClass())
    73             return false;
    74         WMSRequest other = (WMSRequest) obj;
    75         if (Double.doubleToLongBits(pixelPerDegree) != Double
    76                 .doubleToLongBits(other.pixelPerDegree))
    77             return false;
    78         if (xIndex != other.xIndex)
    79             return false;
    80         if (yIndex != other.yIndex)
    81             return false;
    82         if (allowPartialCacheMatch != other.allowPartialCacheMatch)
    83             return false;
    84         return true;
    85     }
    86 
    87     public void setPriority(int priority) {
    88         this.priority = priority;
    89     }
    90 
    91     public int getPriority() {
    92         return priority;
    93     }
    94 
    95     @Override
    96     public int compareTo(WMSRequest o) {
    97         return priority - o.priority;
    98     }
    99 
    100     /**
    101      * Replies the resulting state.
    102      * @return the resulting state
    103      */
    104     public State getState() {
    105         return state;
    106     }
    107 
    108     /**
    109      * Replies the resulting image, if any.
    110      * @return the resulting image, or {@code null}
    111      */
    112     public BufferedImage getImage() {
    113         return image;
    114     }
    115 
    116     /**
    117      * Replies the resulting exception, if any.
    118      * @return the resulting exception, or {@code null}
    119      * @since 7425
    120      */
    121     public WMSException getException() {
    122         return exception;
    123     }
    124 
    125     @Override
    126     public String toString() {
    127         return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex
    128                 + ", pixelPerDegree=" + pixelPerDegree + "]";
    129     }
    130 
    131     public boolean isReal() {
    132         return real;
    133     }
    134 
    135     public boolean isPrecacheOnly() {
    136         return precacheTask != null;
    137     }
    138 
    139     public PrecacheTask getPrecacheTask() {
    140         return precacheTask;
    141     }
    142 
    143     public boolean isAllowPartialCacheMatch() {
    144         return allowPartialCacheMatch;
    145     }
    146 
    147     public boolean hasExactMatch() {
    148         return hasExactMatch;
    149     }
    150 
    151     public void setHasExactMatch(boolean hasExactMatch) {
    152         this.hasExactMatch = hasExactMatch;
    153     }
    154 }
  • src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java

    diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java
    index da5ce92..ac621af 100644
    a b import javax.swing.SwingConstants;  
    1616
    1717import org.openstreetmap.josm.data.Preferences;
    1818import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
     19import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
    1920import org.openstreetmap.josm.gui.layer.ImageryLayer;
    2021import org.openstreetmap.josm.gui.layer.Layer;
    2122import org.openstreetmap.josm.gui.layer.TMSLayer;
    public class ImagerySessionExporter implements SessionLayerExporter {  
    7980        layerElem.setAttribute("version", "0.1");
    8081        ImageryPreferenceEntry e = new ImageryPreferenceEntry(layer.getInfo());
    8182        Map<String,String> data = new LinkedHashMap<>(Preferences.serializeStruct(e, ImageryPreferenceEntry.class));
    82         if (layer instanceof WMSLayer) {
    83             WMSLayer wms = (WMSLayer) layer;
    84             data.put("automatic-downloading", Boolean.toString(wms.hasAutoDownload()));
    85             data.put("automatically-change-resolution", Boolean.toString(wms.isAutoResolution()));
     83        if (layer instanceof AbstractTileSourceLayer) {
     84            AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer;
     85            data.put("automatic-downloading", Boolean.toString(tsLayer.autoLoad));
     86            data.put("automatically-change-resolution", Boolean.toString(tsLayer.autoZoom));
     87            data.put("show-errors", Boolean.toString(tsLayer.showErrors));
    8688        }
    8789        for (Map.Entry<String,String> entry : data.entrySet()) {
    8890            Element attrElem = support.createElement(entry.getKey());
  • src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java

    diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java
    index 95d48ec..3f7346d 100644
    a b import java.util.Map;  
    1010import org.openstreetmap.josm.data.Preferences;
    1111import org.openstreetmap.josm.data.imagery.ImageryInfo;
    1212import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
     13import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
    1314import org.openstreetmap.josm.gui.layer.ImageryLayer;
    1415import org.openstreetmap.josm.gui.layer.Layer;
    15 import org.openstreetmap.josm.gui.layer.WMSLayer;
    1616import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    1717import org.openstreetmap.josm.io.IllegalDataException;
    1818import org.openstreetmap.josm.io.session.SessionReader.ImportSupport;
    public class ImagerySessionImporter implements SessionLayerImporter {  
    4545        ImageryPreferenceEntry prefEntry = Preferences.deserializeStruct(attributes, ImageryPreferenceEntry.class);
    4646        ImageryInfo i = new ImageryInfo(prefEntry);
    4747        ImageryLayer layer = ImageryLayer.create(i);
    48         if (layer instanceof WMSLayer) {
    49             WMSLayer wms = (WMSLayer) layer;
    50             String autoDownload = attributes.get("automatic-downloading");
    51             if (autoDownload != null) {
    52                 wms.setAutoDownload(Boolean.parseBoolean(autoDownload));
    53             }
    54             String autoResolution = attributes.get("automatically-change-resolution");
    55             if (autoResolution != null) {
    56                 wms.setAutoResolution(Boolean.parseBoolean(autoResolution));
     48        if (layer instanceof AbstractTileSourceLayer) {
     49            AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer;
     50            if (attributes.containsKey("automatic-downloading")) {
     51                tsLayer.autoLoad = new Boolean(attributes.get("automatic-downloading")).booleanValue();
     52            }
     53
     54            if (attributes.containsKey("automatically-change-resolution")) {
     55                tsLayer.autoZoom = new Boolean(attributes.get("automatically-change-resolution")).booleanValue();
     56            }
     57
     58            if (attributes.containsKey("show-errors")) {
     59                tsLayer.showErrors = new Boolean(attributes.get("show-errors")).booleanValue();
    5760            }
    5861        }
    5962        return layer;