Changeset 8526 in josm


Ignore:
Timestamp:
2015-06-24T20:57:43+02:00 (4 years ago)
Author:
wiktorn
Message:

Introduce WMS layer based on TMS. (closes: #11255)

HEADS UP: After this patch you need to manually remove JAX-B generated file/class: org/w3/_2001/xmlschema/Adapter1.java to compile the tree again.

  • create AbstractTileSourceLayer based on TMSLayer as a base for TMS, WMS and (future) WMTS layers, (addresses #11459)
  • WMS layer now uses JCS Caching (closes: #7363)
  • introduce new conversion methods in TileSource, that convert both X and Y (lat and lon) in one call. This is necessary for other than PseudoMercator projections
    • introduce TileXY class that represents X and Y indexes of tile in tile matrix/space
    • mark old conversion methods as deprecated
    • refactor JMapViewer and JOSM to new methods
    • change use of Coordinate class to ICoordinate where appropiate
  • extract CachedAttributionBingAerialTileSource to separate file
  • create TemplatedWMSTileSource that provides the WMS Layer with square (according to current projection) tiles (closes: #11572, closes: #7682, addresses: #5454)
  • implemented precaching imagery along GPX track for AbstractTileSourceLayer, so now it work for both - WMS and TMS (closes: #9154)
  • implemented common righ-click menu on map view, as well on layer list (closes #3591)
  • create separate build commands for JMapViewer classes to easily spot, when josm classes are used within JMapViewer
  • remove unnecessary classes of previous WMS implementation - GeorefImage, wms-cache.xsd (and JAXB task from build), WMSCache, WMSRequest, WMSGrabber, HTMLGrabber, WMSException
Location:
trunk
Files:
8 added
7 deleted
15 edited
1 copied

Legend:

Unmodified
Added
Removed
  • trunk/build.xml

    r8510 r8526  
    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"
     
    239224            <exclude name="org/apache/commons/logging/impl/ServletContextCleaner.java"/>
    240225        </javac>
    241         <!-- JMapViewer/JOSM -->
    242         <javac srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java"
     226        <!-- JMapViewer -->
     227        <javac sourcepath="" srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java,org/openstreetmap/josm/**,JOSM.java,gnu/**"
    243228            destdir="build" target="1.7" source="1.7" debug="on" includeantruntime="false" createMissingPackageInfoClass="false" encoding="UTF-8">
    244229            <compilerarg value="-Xlint:cast"/>
     
    256241            <compilerarg value="-XDignore.symbol.file"/>
    257242        </javac>
     243
     244        <!-- JOSM -->
     245        <javac sourcepath="" srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java"
     246            destdir="build" target="1.7" source="1.7" debug="on" includeantruntime="false" createMissingPackageInfoClass="false" encoding="UTF-8">
     247            <compilerarg value="-Xlint:cast"/>
     248            <compilerarg value="-Xlint:deprecation"/>
     249            <compilerarg value="-Xlint:dep-ann"/>
     250            <compilerarg value="-Xlint:divzero"/>
     251            <compilerarg value="-Xlint:empty"/>
     252            <compilerarg value="-Xlint:finally"/>
     253            <compilerarg value="-Xlint:overrides"/>
     254            <!--<compilerarg value="-Xlint:rawtypes"/>-->
     255            <compilerarg value="-Xlint:static"/>
     256            <compilerarg value="-Xlint:try"/>
     257            <compilerarg value="-Xlint:unchecked"/>
     258            <!-- Undocumented argument to ignore "Sun internal proprietary API" warning, see http://stackoverflow.com/a/13862308/2257172 -->
     259            <compilerarg value="-XDignore.symbol.file"/>
     260        </javac>
     261
     262
    258263        <copy todir="build" failonerror="no" includeemptydirs="no">
    259264            <fileset dir="resources"/>
  • trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

    r8513 r8526  
    193193            }
    194194            // object not in cache, so submit work to separate thread
    195             getDownloadExecutor().execute(this);
     195            downloadJobExecutor.execute(this);
    196196        }
    197197    }
     
    230230    protected String getServerKey() {
    231231        return getUrl().getHost();
    232     }
    233 
    234     /**
    235      * this needs to be non-static, so it can be overridden by subclasses
    236      */
    237     protected ThreadPoolExecutor getDownloadExecutor() {
    238         return downloadJobExecutor;
    239232    }
    240233
     
    506499     */
    507500    public void cancelOutstandingTasks() {
    508         ThreadPoolExecutor downloadExecutor = getDownloadExecutor();
    509         for (Runnable r: downloadExecutor.getQueue()) {
    510             if (downloadExecutor.remove(r)) {
    511                 if (r instanceof JCSCachedTileLoaderJob) {
    512                     ((JCSCachedTileLoaderJob<?, ?>) r).handleJobCancellation();
    513                 }
     501        for(Runnable r: downloadJobExecutor.getQueue()) {
     502            if (downloadJobExecutor.remove(r) && r instanceof JCSCachedTileLoaderJob) {
     503                ((JCSCachedTileLoaderJob<?, ?>) r).handleJobCancellation();
    514504            }
    515505        }
  • trunk/src/org/openstreetmap/josm/data/coor/LatLon.java

    r8510 r8526  
    1717import java.util.Locale;
    1818
     19import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    1920import org.openstreetmap.josm.Main;
    2021import org.openstreetmap.josm.data.Bounds;
     
    188189    }
    189190
     191    public LatLon(ICoordinate coor) {
     192        this(coor.getLat(), coor.getLon());
     193    }
     194
     195
    190196    /**
    191197     * Returns the latitude, i.e., the north-south position in degrees.
     
    434440        return true;
    435441    }
     442
     443    public ICoordinate toCoordinate() {
     444        return new org.openstreetmap.gui.jmapviewer.Coordinate(lat(), lon());
     445    }
    436446}
  • trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    r8513 r8526  
    1919import javax.swing.ImageIcon;
    2020
    21 import org.openstreetmap.gui.jmapviewer.Coordinate;
    2221import org.openstreetmap.gui.jmapviewer.OsmMercator;
    2322import org.openstreetmap.gui.jmapviewer.interfaces.Attributed;
     23import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    2424import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
    2525import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik;
     
    575575
    576576    @Override
    577     public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
     577    public String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) {
    578578        return attributionText;
    579579    }
  • trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

    r8510 r8526  
    2929public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache {
    3030
    31     private final ICacheAccess<String, BufferedImageCacheEntry> cache;
    32     private final int connectTimeout;
    33     private final int readTimeout;
    34     private final Map<String, String> headers;
    35     private final 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
     
    5555     * and for TMS imagery
    5656     */
    57     private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getThreadPoolExecutor();
     57    private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getNewThreadPoolExecutor("TMS downloader");
    5858
    59     private final ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
    60 
    61     private static ThreadPoolExecutor getThreadPoolExecutor() {
    62         return new ThreadPoolExecutor(
    63                 THREAD_LIMIT.get().intValue(), // keep the thread number constant
    64                 THREAD_LIMIT.get().intValue(), // do not this number of threads
    65                 30, // keepalive for thread
    66                 TimeUnit.SECONDS,
    67                 new HostLimitQueue(HOST_LIMIT.get().intValue()),
    68                 JCSCachedTileLoaderJob.getNamedThreadFactory("TMS downloader")
    69                 );
    70     }
     59    private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
    7160
    7261    /**
     
    9281    }
    9382
     83    /**
     84     * @param name name of the threads
     85     * @param workers number of worker thread to keep
     86     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue
     87     */
     88    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name, int workers) {
     89        return new ThreadPoolExecutor(
     90                workers, // keep the thread number constant
     91                workers, // do not this number of threads
     92                30, // keepalive for thread
     93                TimeUnit.SECONDS,
     94                new HostLimitQueue(HOST_LIMIT.get().intValue()),
     95                JCSCachedTileLoaderJob.getNamedThreadFactory(name)
     96                );
     97    }
     98
     99    /**
     100     * @param name name of threads
     101     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue, with default number of threads
     102     */
     103    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name) {
     104        return getNewThreadPoolExecutor(name, THREAD_LIMIT.get().intValue());
     105    }
     106
    94107    @Override
    95108    public TileJob createTileLoaderJob(Tile tile) {
    96109        return new TMSCachedTileLoaderJob(listener, tile, cache,
    97                 connectTimeout, readTimeout, headers, downloadExecutor);
     110                connectTimeout, readTimeout, headers, getDownloadExecutor());
    98111    }
    99112
     
    141154        }
    142155    }
     156
     157    /**
     158     * Sets the download executor that will be used to download tiles instead of default one.
     159     * You can use {@link #getNewThreadPoolExecutor} to create a new download executor with separate
     160     * queue from default.
     161     *
     162     * @param downloadExecutor
     163     */
     164    public void setDownloadExecutor(ThreadPoolExecutor downloadExecutor) {
     165        this.downloadExecutor = downloadExecutor;
     166    }
     167
     168    /**
     169     * @return download executor that is used by this factory
     170     */
     171    public ThreadPoolExecutor getDownloadExecutor() {
     172        return downloadExecutor;
     173    }
     174
    143175}
  • trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

    r8510 r8526  
    2727import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    2828import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
     29import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    2930import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
    3031import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     
    7071                }
    7172                try {
    72                     TileSource source = TMSLayer.getTileSource(info);
     73                    TileSource source = TMSLayer.getTileSourceStatic(info);
    7374                    if (source != null) {
    7475                        sources.add(source);
     
    124125
    125126    // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
    126     private Coordinate iSelectionRectStart;
    127     private Coordinate iSelectionRectEnd;
     127    private ICoordinate iSelectionRectStart;
     128    private ICoordinate iSelectionRectEnd;
    128129
    129130    /**
  • trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    r8525 r8526  
    55
    66import java.awt.Color;
     7import java.awt.Component;
    78import java.awt.Font;
    89import java.awt.Graphics;
     
    1920import java.io.File;
    2021import java.io.IOException;
    21 import java.io.StringReader;
    22 import java.net.URL;
    2322import java.text.SimpleDateFormat;
    2423import java.util.ArrayList;
     
    2625import java.util.Comparator;
    2726import java.util.Date;
    28 import java.util.HashMap;
    2927import java.util.LinkedList;
    3028import java.util.List;
    3129import java.util.Map;
    3230import 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;
     31import java.util.Set;
     32import java.util.concurrent.ConcurrentSkipListSet;
    3733
    3834import javax.swing.AbstractAction;
     
    5248import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    5349import org.openstreetmap.gui.jmapviewer.Tile;
     50import org.openstreetmap.gui.jmapviewer.TileXY;
    5451import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     52import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    5553import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
    5654import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    5755import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    5856import 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;
    62 import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
    6357import org.openstreetmap.josm.Main;
    6458import org.openstreetmap.josm.actions.RenameLayerAction;
     59import org.openstreetmap.josm.actions.SaveActionBase;
    6560import org.openstreetmap.josm.data.Bounds;
    66 import org.openstreetmap.josm.data.Version;
    6761import org.openstreetmap.josm.data.coor.EastNorth;
    6862import org.openstreetmap.josm.data.coor.LatLon;
    6963import org.openstreetmap.josm.data.imagery.ImageryInfo;
    70 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    7164import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     65import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
    7266import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    7367import org.openstreetmap.josm.data.preferences.BooleanProperty;
    7468import org.openstreetmap.josm.data.preferences.IntegerProperty;
    75 import org.openstreetmap.josm.data.preferences.StringProperty;
    76 import org.openstreetmap.josm.data.projection.Projection;
    7769import org.openstreetmap.josm.gui.ExtendedDialog;
    7870import org.openstreetmap.josm.gui.MapFrame;
     
    8476import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    8577import 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;
     78import org.openstreetmap.josm.io.WMSLayerImporter;
    9079import org.openstreetmap.josm.tools.GBC;
    91 import org.openstreetmap.josm.tools.Utils;
    92 import org.xml.sax.InputSource;
    93 import org.xml.sax.SAXException;
    9480
    9581/**
    96  * Class that displays a slippy map layer.
    9782 *
    98  * @author Frederik Ramm
    99  * @author LuVar &lt;lubomir.varga@freemap.sk&gt;
    100  * @author Dave Hansen &lt;dave@sr71.net&gt;
    101  * @author Upliner &lt;upliner@gmail.com&gt;
     83 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
     84 *
     85 * It implements all standard functions of tilesource based layers: autozoom,  tile reloads, layer saving, loading,etc.
     86 *
     87 * @author Wiktor Niesiobędzki
     88 * @since TODO
    10289 *
    10390 */
    104 public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
    105     public static final String PREFERENCE_PREFIX   = "imagery.tms";
    106 
     91public abstract class AbstractTileSourceLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
     92    private static final String PREFERENCE_PREFIX   = "imagery.generic";
     93
     94    /** maximum zoom level supported */
    10795    public static final int MAX_ZOOM = 30;
     96    /** minium zoom level supported */
    10897    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 
     98    private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
     99
     100    /** do set autozoom when creating a new layer */
    112101    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
     102    /** do set autoload when creating a new layer */
    113103    public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
     104    /** do set showerrors when creating a new layer */
    114105    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;
    120     static {
    121         String defPath = null;
    122         try {
    123             defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
    124         } catch (SecurityException e) {
    125             Main.warn(e);
    126         }
    127         PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
    128     }
    129 
    130     private final class ShowTileInfoAction extends AbstractAction {
    131         private ShowTileInfoAction() {
    132             super(tr("Show Tile Info"));
    133         }
    134 
    135         private String getSizeString(int size) {
    136             return new StringBuilder().append(size).append("x").append(size).toString();
    137         }
    138 
    139         private JTextField createTextField(String text) {
    140             JTextField ret = new JTextField(text);
    141             ret.setEditable(false);
    142             ret.setBorder(BorderFactory.createEmptyBorder());
    143             return ret;
    144         }
    145 
    146         @Override
    147         public void actionPerformed(ActionEvent ae) {
    148             if (clickedTile != null) {
    149                 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
    150                 JPanel panel = new JPanel(new GridBagLayout());
    151                 Rectangle displaySize = tileToRect(clickedTile);
    152                 String url = "";
    153                 try {
    154                     url = clickedTile.getUrl();
    155                 } catch (IOException e) {
    156                     // silence exceptions
    157                     if (Main.isTraceEnabled()) {
    158                         Main.trace(e.getMessage());
    159                     }
    160                 }
    161 
    162                 String[][] content = {
    163                         {"Tile name", clickedTile.getKey()},
    164                         {"Tile url", url},
    165                         {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
    166                         {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
    167                 };
    168 
    169                 for (String[] entry: content) {
    170                     panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
    171                     panel.add(GBC.glue(5, 0), GBC.std());
    172                     panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
    173                 }
    174 
    175                 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
    176                     panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
    177                     panel.add(GBC.glue(5, 0), GBC.std());
    178                     String value = e.getValue();
    179                     if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
    180                         value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
    181                     }
    182                     panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
    183                 }
    184                 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
    185                 ed.setContent(panel);
    186                 ed.showDialog();
    187             }
    188         }
    189     }
    190 
    191     /**
    192      * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map
    193      *
    194      */
    195     public interface TileLoaderFactory {
    196         /**
    197          * @param listener object that will be notified, when tile has finished loading
    198          * @return TileLoader that will notify the listener
    199          */
    200         TileLoader makeTileLoader(TileLoaderListener listener);
    201 
    202         /**
    203          * @param listener object that will be notified, when tile has finished loading
    204          * @param headers HTTP headers that should be sent by TileLoader to tile server
    205          * @return TileLoader that will notify the listener
    206          */
    207         TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
    208     }
     106    /** minimum zoom level to show to user */
     107    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
     108    /** maximum zoom level to show to user */
     109    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
     110
     111    //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
     112    /**
     113     * Zoomlevel at which tiles is currently downloaded.
     114     * Initial zoom lvl is set to bestZoom
     115     */
     116    public int currentZoomLevel;
     117    private boolean needRedraw;
     118
     119    private AttributionSupport attribution = new AttributionSupport();
     120    Tile showMetadataTile;
     121
     122    // needed public access for session exporter
     123    /** if layers changes automatically, when user zooms in */
     124    public boolean autoZoom;
     125    /** if layer automatically loads new tiles */
     126    public boolean autoLoad;
     127    /** if layer should show errors on tiles */
     128    public boolean showErrors;
    209129
    210130    protected TileCache tileCache;
    211131    protected TileSource tileSource;
     132    //protected  tileMatrix;
    212133    protected TileLoader tileLoader;
    213134
    214 
    215     public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
    216         @Override
    217         public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
    218             Map<String, String> headers = new HashMap<>();
    219             headers.put("User-Agent", Version.getInstance().getFullAgentString());
    220             headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
    221             if (inputHeaders != null)
    222                 headers.putAll(inputHeaders);
    223 
    224             try {
    225                 return new TMSCachedTileLoader(listener, "TMS",
    226                         Main.pref.getInteger("socket.timeout.connect", 15) * 1000,
    227                         Main.pref.getInteger("socket.timeout.read", 30) * 1000,
    228                         headers,
    229                         PROP_TILECACHE_DIR.get());
    230             } catch (IOException e) {
    231                 Main.warn(e);
    232             }
    233             return null;
    234         }
    235 
    236         @Override
    237         public TileLoader makeTileLoader(TileLoaderListener listener) {
    238             return makeTileLoader(listener, null);
    239         }
    240     };
    241 
    242     /**
    243      * Plugins that wish to set custom tile loader should call this method
    244      */
    245 
    246     public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
    247         TMSLayer.loaderFactory = loaderFactory;
    248     }
     135    /**
     136     * Creates Tile Source based Imagery Layer based on Imagery Info
     137     * @param info
     138     * @param tileSource
     139     */
     140    public AbstractTileSourceLayer(ImageryInfo info) {
     141        super(info);
     142
     143        if(!isProjectionSupported(Main.getProjection())) {
     144            JOptionPane.showMessageDialog(Main.parent,
     145                    tr("This layer do not support the projection {0}.\n{1}\n"
     146                            + "Change the projection or remove the layer.",
     147                            Main.getProjection().toCode(), nameSupportedProjections()),
     148                            tr("Warning"),
     149                            JOptionPane.WARNING_MESSAGE);
     150        }
     151        setBackgroundLayer(true);
     152        this.setVisible(true);
     153
     154        initTileSource(getTileSource(info));
     155        MapView.addZoomChangeListener(this);
     156    }
     157
     158    protected abstract TileLoaderFactory getTileLoaderFactory();
     159
     160    /**
     161     *
     162     * @param info
     163     * @return TileSource for specified ImageryInfo
     164     * @throws IllegalArgumentException when Imagery is not supported by layer
     165     */
     166    protected abstract TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException;
     167
     168    protected abstract Map<String, String> getHeaders(TileSource tileSource);
     169
     170    protected void initTileSource(TileSource tileMatrix) {
     171        this.tileSource = tileMatrix;
     172        attribution.initialize(tileMatrix);
     173
     174        currentZoomLevel = getBestZoom();
     175
     176        Map<String, String> headers = getHeaders(tileMatrix);
     177
     178        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
     179        if (tileLoader instanceof TMSCachedTileLoader) {
     180            tileCache = (TileCache) tileLoader;
     181        } else {
     182            tileCache = new MemoryTileCache();
     183        }
     184        if (tileLoader == null)
     185            tileLoader = new OsmTileLoader(this);
     186    }
     187
     188
    249189
    250190    @Override
     
    279219        tileCache.clear();
    280220        if (tileLoader instanceof CachedTileLoader) {
    281             ((CachedTileLoader) tileLoader).clearCache(tileSource);
    282         }
    283         redraw();
    284     }
    285 
    286     /**
    287      * Zoomlevel at which tiles is currently downloaded.
    288      * Initial zoom lvl is set to bestZoom
    289      */
    290     public int currentZoomLevel;
    291 
    292     private Tile clickedTile;
    293     private boolean needRedraw;
    294     private JPopupMenu tileOptionMenu;
    295     private Tile showMetadataTile;
    296     private AttributionSupport attribution = new AttributionSupport();
    297     private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
    298 
    299     protected boolean autoZoom;
    300     protected boolean autoLoad;
    301     protected boolean showErrors;
     221            ((CachedTileLoader)tileLoader).clearCache(tileSource);
     222        }
     223    }
     224
    302225
    303226    /**
     
    312235    }
    313236
    314     protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
    315         if (maxZoomLvl > MAX_ZOOM) {
    316             maxZoomLvl = MAX_ZOOM;
    317         }
    318         if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
    319             maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
    320         }
    321         if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
    322             maxZoomLvl = ts.getMaxZoom();
    323         }
    324         return maxZoomLvl;
    325     }
    326 
    327     public static int getMaxZoomLvl(TileSource ts) {
    328         return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
    329     }
    330 
    331     public static void setMaxZoomLvl(int maxZoomLvl) {
    332         Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));
    333         PROP_MAX_ZOOM_LVL.put(newMaxZoom);
    334     }
    335 
    336     static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
    337         if (minZoomLvl < MIN_ZOOM) {
    338             /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
    339             minZoomLvl = MIN_ZOOM;
    340         }
    341         if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
    342             /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
    343             minZoomLvl = getMaxZoomLvl(ts);
    344         }
    345         if (ts != null && ts.getMinZoom() > minZoomLvl) {
    346             /*Main.debug("Increasing min. zoom level to match tile source");*/
    347             minZoomLvl = ts.getMinZoom();
    348         }
    349         return minZoomLvl;
    350     }
    351 
    352     public static int getMinZoomLvl(TileSource ts) {
    353         return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
    354     }
    355 
    356     public static void setMinZoomLvl(int minZoomLvl) {
    357         minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
    358         PROP_MIN_ZOOM_LVL.put(minZoomLvl);
    359     }
    360 
    361     private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
    362 
    363         public CachedAttributionBingAerialTileSource(ImageryInfo info) {
    364             super(info);
    365         }
    366 
    367         class BingAttributionData extends CacheCustomContent<IOException> {
    368 
    369             public BingAttributionData() {
    370                 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
    371             }
    372 
    373             @Override
    374             protected byte[] updateData() throws IOException {
    375                 URL u = getAttributionUrl();
    376                 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
    377                     String r = scanner.useDelimiter("\\A").next();
    378                     Main.info("Successfully loaded Bing attribution data.");
    379                     return r.getBytes("UTF-8");
    380                 }
    381             }
    382         }
    383 
    384         @Override
    385         protected Callable<List<Attribution>> getAttributionLoaderCallable() {
    386             return new Callable<List<Attribution>>() {
    387 
    388                 @Override
    389                 public List<Attribution> call() throws Exception {
    390                     BingAttributionData attributionLoader = new BingAttributionData();
    391                     int waitTimeSec = 1;
    392                     while (true) {
    393                         try {
    394                             String xml = attributionLoader.updateIfRequiredString();
    395                             return parseAttributionText(new InputSource(new StringReader(xml)));
    396                         } catch (IOException ex) {
    397                             Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
    398                             Thread.sleep(waitTimeSec * 1000L);
    399                             waitTimeSec *= 2;
    400                         }
    401                     }
    402                 }
    403             };
    404         }
    405     }
    406 
    407     /**
    408      * Creates and returns a new TileSource instance depending on the {@link ImageryType}
    409      * of the passed ImageryInfo object.
    410      *
    411      * If no appropriate TileSource is found, null is returned.
    412      * Currently supported ImageryType are {@link ImageryType#TMS},
    413      * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
    414      *
    415      * @param info imagery info
    416      * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
    417      * @throws IllegalArgumentException if url from imagery info is null or invalid
    418      */
    419     public static TileSource getTileSource(ImageryInfo info) {
    420         if (info.getImageryType() == ImageryType.TMS) {
    421             checkUrl(info.getUrl());
    422             TMSTileSource t = new TemplatedTMSTileSource(info);
    423             info.setAttribution(t);
    424             return t;
    425         } else if (info.getImageryType() == ImageryType.BING) {
    426             return new CachedAttributionBingAerialTileSource(info);
    427         } else if (info.getImageryType() == ImageryType.SCANEX) {
    428             return new ScanexTileSource(info);
    429         }
    430         return null;
    431     }
    432 
    433     /**
    434      * Checks validity of given URL.
    435      * @param url URL to check
    436      * @throws IllegalArgumentException if url is null or invalid
    437      */
    438     public static void checkUrl(String url) {
    439         CheckParameterUtil.ensureParameterNotNull(url, "url");
    440         Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
    441         while (m.find()) {
    442             boolean isSupportedPattern = false;
    443             for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
    444                 if (m.group().matches(pattern)) {
    445                     isSupportedPattern = true;
    446                     break;
    447                 }
    448             }
    449             if (!isSupportedPattern) {
    450                 throw new IllegalArgumentException(
    451                         tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
    452             }
    453         }
    454     }
    455 
    456     private void initTileSource(TileSource tileSource) {
    457         this.tileSource = tileSource;
    458         attribution.initialize(tileSource);
    459 
    460         currentZoomLevel = getBestZoom();
    461 
    462         Map<String, String> headers = null;
    463         if (tileSource instanceof TemplatedTMSTileSource) {
    464             headers = (((TemplatedTMSTileSource) tileSource).getHeaders());
    465         }
    466 
    467         tileLoader = loaderFactory.makeTileLoader(this, headers);
    468         if (tileLoader instanceof TMSCachedTileLoader) {
    469             tileCache = (TileCache) tileLoader;
    470         } else {
    471             tileCache = new MemoryTileCache();
    472         }
    473         if (tileLoader == null)
    474             tileLoader = new OsmTileLoader(this);
    475     }
    476 
    477237    /**
    478238     * Marks layer as needing redraw on offset change
     
    483243        needRedraw = true;
    484244    }
     245
     246
    485247    /**
    486248     * Returns average number of screen pixels per tile pixel for current mapview
     
    491253        LatLon topLeft = mv.getLatLon(0, 0);
    492254        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
    493         double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
    494         double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
    495         double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
    496         double y2 = tileSource.latToTileY(botRight.lat(), zoom);
     255        TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
     256        TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
    497257
    498258        int screenPixels = mv.getWidth()*mv.getHeight();
    499         double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
     259        double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
    500260        if (screenPixels == 0 || tilePixels == 0) return 1;
    501261        return screenPixels/tilePixels;
    502262    }
    503263
    504     private int getBestZoom() {
     264    private final int getBestZoom() {
    505265        double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
    506266        double result = Math.log(factor)/Math.log(2)/2+1;
     
    515275         * getScaleFactor(...) is supposed to be between 0.75 and 3
    516276         */
    517         int intResult = (int) Math.floor(result);
     277        int intResult = (int)Math.floor(result);
    518278        if (intResult > getMaxZoomLvl())
    519279            return getMaxZoomLvl();
     
    523283    }
    524284
    525     @SuppressWarnings("serial")
    526     public TMSLayer(ImageryInfo info) {
    527         super(info);
    528 
    529         if (!isProjectionSupported(Main.getProjection())) {
    530             JOptionPane.showMessageDialog(Main.parent,
    531                     tr("TMS layers do not support the projection {0}.\n{1}\n"
    532                             + "Change the projection or remove the layer.",
    533                             Main.getProjection().toCode(), nameSupportedProjections()),
    534                             tr("Warning"),
    535                             JOptionPane.WARNING_MESSAGE);
    536         }
    537 
    538         setBackgroundLayer(true);
    539         this.setVisible(true);
    540 
    541         TileSource source = getTileSource(info);
    542         if (source == null)
    543             throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
    544         initTileSource(source);
    545 
    546         MapView.addZoomChangeListener(this);
    547     }
    548 
    549     /**
    550      * Adds a context menu to the mapView.
     285
     286    private final static boolean actionSupportLayers(List<Layer> layers) {
     287        return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
     288    }
     289
     290    private class AutoZoomAction extends AbstractAction implements LayerAction {
     291        public AutoZoomAction() {
     292            super(tr("Auto Zoom"));
     293        }
     294        @Override
     295        public void actionPerformed(ActionEvent ae) {
     296            autoZoom = !autoZoom;
     297        }
     298
     299        public Component createMenuComponent() {
     300            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     301            item.setSelected(autoZoom);
     302            return item;
     303        }
     304        @Override
     305        public boolean supportLayers(List<Layer> layers) {
     306            return actionSupportLayers(layers);
     307        }
     308
     309    }
     310
     311    private class AutoLoadTilesAction extends AbstractAction implements LayerAction {
     312        public AutoLoadTilesAction() {
     313            super(tr("Auto load tiles"));
     314        }
     315        @Override
     316        public void actionPerformed(ActionEvent ae) {
     317            autoLoad= !autoLoad;
     318        }
     319
     320        public Component createMenuComponent() {
     321            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     322            item.setSelected(autoLoad);
     323            return item;
     324        }
     325        @Override
     326        public boolean supportLayers(List<Layer> layers) {
     327            return actionSupportLayers(layers);
     328        }
     329    }
     330
     331    private class LoadAllTilesAction extends AbstractAction {
     332        public LoadAllTilesAction() {
     333            super(tr("Load All Tiles"));
     334        }
     335        @Override
     336        public void actionPerformed(ActionEvent ae) {
     337            loadAllTiles(true);
     338            redraw();
     339        }
     340    }
     341
     342    private class LoadErroneusTilesAction extends AbstractAction {
     343        public LoadErroneusTilesAction() {
     344            super(tr("Load All Error Tiles"));
     345        }
     346
     347        @Override
     348        public void actionPerformed(ActionEvent ae) {
     349            loadAllErrorTiles(true);
     350            redraw();
     351        }
     352    }
     353
     354    private class ZoomToNativeLevelAction extends AbstractAction {
     355        public ZoomToNativeLevelAction() {
     356            super(tr("Zoom to native resolution"));
     357        }
     358        @Override
     359        public void actionPerformed(ActionEvent ae) {
     360            double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
     361            Main.map.mapView.zoomToFactor(new_factor);
     362            redraw();
     363        }
     364    }
     365
     366    private class ZoomToBestAction extends AbstractAction {
     367        public ZoomToBestAction() {
     368            super(tr("Change resolution"));
     369        }
     370        @Override
     371        public void actionPerformed(ActionEvent ae) {
     372            setZoomLevel(getBestZoom());
     373        }
     374    }
     375
     376    /*
     377     * Simple class to keep clickedTile within hookUpMapView
     378     */
     379    private class TileHolder {
     380        private Tile t = null;
     381
     382        public Tile getTile() {
     383            return t;
     384        }
     385
     386        public void setTile(Tile t) {
     387            this.t = t;
     388        }
     389    }
     390
     391    /**
     392     * Creates popup menu items and binds to mouse actions
    551393     */
    552394    @Override
    553395    public void hookUpMapView() {
    554         tileOptionMenu = new JPopupMenu();
     396        // keep them final here, so we avoid namespace clutter in the class
     397        final JPopupMenu tileOptionMenu = new JPopupMenu();
     398        final TileHolder clickedTileHolder = new TileHolder();
    555399
    556400        autoZoom = PROP_DEFAULT_AUTOZOOM.get();
    557401        JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
    558         autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
    559             @Override
    560             public void actionPerformed(ActionEvent ae) {
    561                 autoZoom = !autoZoom;
    562             }
    563         });
     402        autoZoomPopup.setAction(new AutoZoomAction());
    564403        autoZoomPopup.setSelected(autoZoom);
    565404        tileOptionMenu.add(autoZoomPopup);
     
    567406        autoLoad = PROP_DEFAULT_AUTOLOAD.get();
    568407        JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
    569         autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
    570             @Override
    571             public void actionPerformed(ActionEvent ae) {
    572                 autoLoad = !autoLoad;
    573             }
    574         });
     408        autoLoadPopup.setAction(new AutoLoadTilesAction());
    575409        autoLoadPopup.setSelected(autoLoad);
    576410        tileOptionMenu.add(autoLoadPopup);
     
    590424            @Override
    591425            public void actionPerformed(ActionEvent ae) {
     426                Tile clickedTile = clickedTileHolder.getTile();
    592427                if (clickedTile != null) {
    593428                    loadTile(clickedTile, true);
     
    597432        }));
    598433
    599         tileOptionMenu.add(new JMenuItem(new ShowTileInfoAction()));
    600 
    601434        tileOptionMenu.add(new JMenuItem(new AbstractAction(
    602                 tr("Request Update")) {
     435                tr("Show Tile Info")) {
     436            private String getSizeString(int size) {
     437                StringBuilder ret = new StringBuilder();
     438                return ret.append(size).append("x").append(size).toString();
     439            }
     440
     441            private JTextField createTextField(String text) {
     442                JTextField ret = new JTextField(text);
     443                ret.setEditable(false);
     444                ret.setBorder(BorderFactory.createEmptyBorder());
     445                return ret;
     446            }
     447
    603448            @Override
    604449            public void actionPerformed(ActionEvent ae) {
     450                Tile clickedTile = clickedTileHolder.getTile();
    605451                if (clickedTile != null) {
    606                     clickedTile.setLoaded(false);
    607                     tileLoader.createTileLoaderJob(clickedTile).submit(true);
     452                    ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
     453                    JPanel panel = new JPanel(new GridBagLayout());
     454                    Rectangle displaySize = tileToRect(clickedTile);
     455                    String url = "";
     456                    try {
     457                        url = clickedTile.getUrl();
     458                    } catch (IOException e) {
     459                        // silence exceptions
     460                    }
     461
     462                    String[][] content = {
     463                            {"Tile name", clickedTile.getKey()},
     464                            {"Tile url", url},
     465                            {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
     466                            {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
     467                    };
     468
     469                    for (String[] entry: content) {
     470                        panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
     471                        panel.add(GBC.glue(5,0), GBC.std());
     472                        panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
     473                    }
     474
     475                    for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
     476                        panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
     477                        panel.add(GBC.glue(5,0), GBC.std());
     478                        String value = e.getValue();
     479                        if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
     480                            value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
     481                        }
     482                        panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
     483
     484                    }
     485                    ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
     486                    ed.setContent(panel);
     487                    ed.showDialog();
    608488                }
    609489            }
    610490        }));
    611491
    612         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    613                 tr("Load All Tiles")) {
    614             @Override
    615             public void actionPerformed(ActionEvent ae) {
    616                 loadAllTiles(true);
    617                 redraw();
    618             }
    619         }));
    620 
    621         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    622                 tr("Load All Error Tiles")) {
    623             @Override
    624             public void actionPerformed(ActionEvent ae) {
    625                 loadAllErrorTiles(true);
    626                 redraw();
    627             }
    628         }));
     492        tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction()));
     493        tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction()));
    629494
    630495        // increase and decrease commands
     
    663528                new PleaseWaitRunnable(tr("Flush Tile Cache")) {
    664529                    @Override
    665                     protected void realRun() throws SAXException, IOException,
    666                             OsmTransferException {
     530                    protected void realRun() {
    667531                        clearTileCache(getProgressMonitor());
    668532                    }
     
    684548                if (!isVisible()) return;
    685549                if (e.getButton() == MouseEvent.BUTTON3) {
    686                     clickedTile = getTileForPixelpos(e.getX(), e.getY());
     550                    clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
    687551                    tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
    688552                } else if (e.getButton() == MouseEvent.BUTTON1) {
     
    706570            @Override
    707571            public void layerRemoved(Layer oldLayer) {
    708                 if (oldLayer == TMSLayer.this) {
     572                if (oldLayer == AbstractTileSourceLayer.this) {
    709573                    Main.map.mapView.removeMouseListener(adapter);
    710                     MapView.removeZoomChangeListener(TMSLayer.this);
    711574                    MapView.removeLayerChangeListener(this);
     575                    MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
    712576                }
    713577            }
    714578        });
    715579    }
     580
     581    /**
     582     * Checks zoom level against settings
     583     * @param maxZoomLvl zoom level to check
     584     * @param ts tile source to crosscheck with
     585     * @return maximum zoom level, not higher than supported by tilesource nor set by the user
     586     */
     587    public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
     588        if(maxZoomLvl > MAX_ZOOM) {
     589            maxZoomLvl = MAX_ZOOM;
     590        }
     591        if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
     592            maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
     593        }
     594        if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
     595            maxZoomLvl = ts.getMaxZoom();
     596        }
     597        return maxZoomLvl;
     598    }
     599
     600    /**
     601     * Checks zoom level against settings
     602     * @param minZoomLvl zoom level to check
     603     * @param ts tile source to crosscheck with
     604     * @return minimum zoom level, not higher than supported by tilesource nor set by the user
     605     */
     606    public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
     607        if(minZoomLvl < MIN_ZOOM) {
     608            minZoomLvl = MIN_ZOOM;
     609        }
     610        if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
     611            minZoomLvl = getMaxZoomLvl(ts);
     612        }
     613        if (ts != null && ts.getMinZoom() > minZoomLvl) {
     614            minZoomLvl = ts.getMinZoom();
     615        }
     616        return minZoomLvl;
     617    }
     618
     619
     620    /**
     621     * @param ts TileSource for which we want to know maximum zoom level
     622     * @return maximum max zoom level, that will be shown on layer
     623     */
     624    public static int getMaxZoomLvl(TileSource ts) {
     625        return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
     626    }
     627
     628    /**
     629     * @param ts TileSource for which we want to know minimum zoom level
     630     * @return minimum zoom level, that will be shown on layer
     631     */
     632    public static int getMinZoomLvl(TileSource ts) {
     633        return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
     634    }
     635
     636
     637    /**
     638     * Sets maximum zoom level, that layer will attempt show
     639     * @param maxZoomLvl
     640     */
     641    public static void setMaxZoomLvl(int maxZoomLvl) {
     642        maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
     643        PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
     644    }
     645
     646    /**
     647     * Sets minimum zoom level, that layer will attempt show
     648     * @param minZoomLvl
     649     */
     650    public static void setMinZoomLvl(int minZoomLvl) {
     651        minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
     652        PROP_MIN_ZOOM_LVL.put(minZoomLvl);
     653    }
     654
    716655
    717656    /**
     
    724663            Main.debug("zoomChanged(): " + currentZoomLevel);
    725664        }
     665        if (tileLoader instanceof TMSCachedTileLoader) {
     666            ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
     667        }
    726668        needRedraw = true;
    727         if (tileLoader instanceof TMSCachedTileLoader) {
    728             ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
    729         }
    730669    }
    731670
     
    742681
    743682    /**
     683     *
     684     * @return if its allowed to zoom in
     685     */
     686    public boolean zoomIncreaseAllowed() {
     687        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
     688        if (Main.isDebugEnabled()) {
     689            Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
     690        }
     691        return zia;
     692    }
     693
     694    /**
    744695     * Zoom in, go closer to map.
    745696     *
    746697     * @return    true, if zoom increasing was successful, false otherwise
    747698     */
    748     public boolean zoomIncreaseAllowed() {
    749         boolean zia = currentZoomLevel < this.getMaxZoomLvl();
    750         if (Main.isDebugEnabled()) {
    751             Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
    752         }
    753         return zia;
    754     }
    755 
    756699    public boolean increaseZoomLevel() {
    757700        if (zoomIncreaseAllowed()) {
     
    769712    }
    770713
     714    /**
     715     * Sets the zoom level of the layer
     716     * @param zoom zoom level
     717     * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
     718     */
    771719    public boolean setZoomLevel(int zoom) {
    772720        if (zoom == currentZoomLevel) return true;
     
    837785     */
    838786    private Tile getTile(int x, int y, int zoom) {
    839         int max = 1 << zoom;
     787        int max = (1 << zoom);
    840788        if (x < 0 || x >= max || y < 0 || y >= max)
    841789            return null;
     
    850798        if (tile.isLoading())
    851799            return false;
    852         tileLoader.createTileLoaderJob(tile).submit(force);
     800        tileLoader.createTileLoaderJob(tile).submit();
    853801        return true;
    854802    }
    855803
    856     private void loadAllTiles(boolean force) {
     804    private TileSet getVisibleTileSet() {
    857805        MapView mv = Main.map.mapView;
    858806        EastNorth topLeft = mv.getEastNorth(0, 0);
    859807        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    860 
    861         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
     808        return new TileSet(topLeft, botRight, currentZoomLevel);
     809    }
     810
     811    private void loadAllTiles(boolean force) {
     812        TileSet ts = getVisibleTileSet();
    862813
    863814        // if there is more than 18 tiles on screen in any direction, do not
     
    871822
    872823    private void loadAllErrorTiles(boolean force) {
    873         MapView mv = Main.map.mapView;
    874         EastNorth topLeft = mv.getEastNorth(0, 0);
    875         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    876 
    877         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
    878 
     824        TileSet ts = getVisibleTileSet();
    879825        ts.loadAllErrorTiles(force);
    880826    }
     
    914860            return null;
    915861        return img;
    916     }
    917 
    918     private LatLon tileLatLon(Tile t) {
    919         int zoom = t.getZoom();
    920         return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
    921                 tileSource.tileXToLon(t.getXtile(), zoom));
    922862    }
    923863
     
    968908        // And how many pixels into the image itself does that
    969909        // correlate to?
    970         int img_x_offset = (int) (screen_x_offset * imageXScaling + 0.5);
    971         int img_y_offset = (int) (screen_y_offset * imageYScaling + 0.5);
     910        int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
     911        int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
    972912        // Now calculate the other corner of the image that we need
    973913        // by scaling the 'target' rectangle's dimensions.
    974         int img_x_end = img_x_offset + (int) (target.getWidth() * imageXScaling + 0.5);
    975         int img_y_end = img_y_offset + (int) (target.getHeight() * imageYScaling + 0.5);
     914        int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
     915        int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
    976916
    977917        if (Main.isDebugEnabled()) {
     
    1035975        Color oldColor = g.getColor();
    1036976        g.setColor(Color.black);
    1037         g.drawString(text, x+1, y+1);
     977        g.drawString(text,x+1,y+1);
    1038978        g.setColor(oldColor);
    1039         g.drawString(text, x, y);
     979        g.drawString(text,x,y);
    1040980    }
    1041981
     
    1055995            }
    1056996        }*/
    1057 
    1058         if (tile == showMetadataTile) {
    1059             String md = tile.toString();
    1060             if (md != null) {
    1061                 myDrawString(g, md, p.x + 2, texty);
    1062                 texty += 1 + fontHeight;
    1063             }
    1064             Map<String, String> meta = tile.getMetadata();
    1065             if (meta != null) {
    1066                 for (Map.Entry<String, String> entry : meta.entrySet()) {
    1067                     myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
    1068                     texty += 1 + fontHeight;
    1069                 }
    1070             }
    1071         }
    1072997
    1073998        /*String tileStatus = tile.getStatus();
     
    11131038
    11141039    private Point pixelPos(Tile t) {
    1115         double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
    1116         LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
    1117         return pixelPos(tmpLL);
     1040        ICoordinate coord = tileSource.tileXYToLatLon(t);
     1041        return pixelPos(new LatLon(coord));
    11181042    }
    11191043
     
    11241048    private Coordinate getShiftedCoord(EastNorth en) {
    11251049        LatLon ll = getShiftedLatLon(en);
    1126         return new Coordinate(ll.lat(), ll.lon());
    1127     }
    1128 
    1129     private final TileSet nullTileSet = new TileSet((LatLon) null, (LatLon) null, 0);
    1130 
     1050        return new Coordinate(ll.lat(),ll.lon());
     1051    }
     1052
     1053    private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
    11311054    private final class TileSet {
    1132         private int x0, x1, y0, y1;
    1133         private int zoom;
    1134         private int tileMax = -1;
     1055        int x0, x1, y0, y1;
     1056        int zoom;
    11351057
    11361058        /**
     
    11381060         */
    11391061        private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
    1140             this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
     1062            this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
    11411063        }
    11421064
     
    11491071                return;
    11501072
    1151             x0 = (int) tileSource.lonToTileX(topLeft.lon(),  zoom);
    1152             y0 = (int) tileSource.latToTileY(topLeft.lat(),  zoom);
    1153             x1 = (int) tileSource.lonToTileX(botRight.lon(), zoom);
    1154             y1 = (int) tileSource.latToTileY(botRight.lat(), zoom);
     1073            TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
     1074            TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
     1075
     1076            x0 = t1.getXIndex();
     1077            y0 = t1.getYIndex();
     1078            x1 = t2.getXIndex();
     1079            y1 = t2.getYIndex();
     1080
    11551081            if (x0 > x1) {
    11561082                int tmp = x0;
     
    11631089                y1 = tmp;
    11641090            }
    1165             tileMax = (int) Math.pow(2.0, zoom);
    1166             if (x0 < 0) {
    1167                 x0 = 0;
    1168             }
    1169             if (y0 < 0) {
    1170                 y0 = 0;
    1171             }
    1172             if (x1 > tileMax) {
    1173                 x1 = tileMax;
    1174             }
    1175             if (y1 > tileMax) {
    1176                 y1 = tileMax;
     1091
     1092            if (x0 < tileSource.getTileXMin(zoom)) {
     1093                x0 = tileSource.getTileXMin(zoom);
     1094            }
     1095            if (y0 < tileSource.getTileYMin(zoom)) {
     1096                y0 = tileSource.getTileYMin(zoom);
     1097            }
     1098            if (x1 > tileSource.getTileXMax(zoom)) {
     1099                x1 = tileSource.getTileXMax(zoom);
     1100            }
     1101            if (y1 > tileSource.getTileYMax(zoom)) {
     1102                y1 = tileSource.getTileYMax(zoom);
    11771103            }
    11781104        }
     
    12211147                    Tile t;
    12221148                    if (create) {
    1223                         t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
     1149                        t = getOrCreateTile(x, y , zoom);
    12241150                    } else {
    1225                         t = getTile(x % tileMax, y % tileMax, zoom);
     1151                        t = getTile(x, y, zoom);
    12261152                    }
    12271153                    if (t != null) {
     
    12421168        }
    12431169
     1170        /**
     1171         * @return comparator, that sorts the tiles from the center to the edge of the current screen
     1172         */
    12441173        private Comparator<Tile> getTileDistanceComparator() {
    1245             final int centerX = (int) Math.ceil((x0 + x1) / 2);
    1246             final int centerY = (int) Math.ceil((y0 + y1) / 2);
     1174            final int centerX = (int) Math.ceil((x0 + x1) / 2d);
     1175            final int centerY = (int) Math.ceil((y0 + y1) / 2d);
    12471176            return new Comparator<Tile>() {
    12481177                private int getDistance(Tile t) {
    12491178                    return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
    12501179                }
    1251 
    12521180                @Override
    12531181                public int compare(Tile o1, Tile o2) {
     
    12581186            };
    12591187        }
     1188
    12601189
    12611190        private void loadAllTiles(boolean force) {
     
    12791208        }
    12801209    }
     1210
    12811211
    12821212    private static class TileSetInfo {
     
    13101240        private final TileSet[] tileSets;
    13111241        private final TileSetInfo[] tileSetInfos;
    1312 
    13131242        public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
    13141243            this.topLeft = topLeft;
     
    13191248            this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
    13201249        }
    1321 
    13221250        public TileSet getTileSet(int zoom) {
    13231251            if (zoom < minZoom)
     
    13391267                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    13401268                if (tsi == null) {
    1341                     tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
     1269                    tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
    13421270                    tileSetInfos[zoom-minZoom] = tsi;
    13431271                }
     
    13881316            }
    13891317            // Do binary search between currentZoomLevel and displayZoomLevel
    1390             while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
     1318            while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
    13911319                zoom = (zoom + displayZoomLevel)/2;
    13921320                tsi = dts.getTileSetInfo(zoom);
     
    14451373                }
    14461374                Tile t2 = tempCornerTile(missed);
    1447                 LatLon topLeft2  = tileLatLon(missed);
    1448                 LatLon botRight2 = tileLatLon(t2);
     1375                LatLon topLeft2  = new LatLon(tileSource.tileXYToLatLon(missed));
     1376                LatLon botRight2 = new LatLon(tileSource.tileXYToLatLon(t2));
    14491377                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
    14501378                // Instantiating large TileSets is expensive.  If there
     
    14941422            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
    14951423            myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
    1496             if (tileLoader instanceof TMSCachedTileLoader) {
    1497                 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
     1424            if(tileLoader instanceof TMSCachedTileLoader) {
     1425                TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
    14981426                int offset = 185;
    1499                 for (String part: cachedTileLoader.getStats().split("\n")) {
    1500                     myDrawString(g, tr("Cache stats: {0}", part), 50, offset += 15);
    1501                 }
     1427                for(String part: cachedTileLoader.getStats().split("\n")) {
     1428                    myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
     1429                }
     1430
    15021431            }
    15031432        }
     
    15461475    public Action[] getMenuEntries() {
    15471476        return new Action[] {
     1477                LayerListDialog.getInstance().createActivateLayerAction(this),
    15481478                LayerListDialog.getInstance().createShowHideLayerAction(),
    15491479                LayerListDialog.getInstance().createDeleteLayerAction(),
     
    15531483                new RenameLayerAction(this.getAssociatedFile(), this),
    15541484                SeparatorLayerAction.INSTANCE,
    1555                 new LayerListPopup.InfoAction(this) };
     1485                new AutoLoadTilesAction(),
     1486                new AutoZoomAction(),
     1487                new ZoomToBestAction(),
     1488                new ZoomToNativeLevelAction(),
     1489                new LoadErroneusTilesAction(),
     1490                new LoadAllTilesAction(),
     1491                new LayerListPopup.InfoAction(this)
     1492        };
    15561493    }
    15571494
    15581495    @Override
    15591496    public String getToolTipText() {
    1560         return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
     1497        if(autoLoad) {
     1498            return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     1499        } else {
     1500            return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
     1501        }
    15611502    }
    15621503
     
    15701511    }
    15711512
     1513    /**
     1514     * Task responsible for precaching imagery along the gpx track
     1515     *
     1516     */
     1517    public class PrecacheTask implements TileLoaderListener {
     1518        private final ProgressMonitor progressMonitor;
     1519        private volatile int totalCount;
     1520        private volatile int processedCount = 0;
     1521        private TileLoader tileLoader;
     1522
     1523        /**
     1524         * @param progressMonitor that will be notified about progess of the task
     1525         */
     1526        public PrecacheTask(ProgressMonitor progressMonitor) {
     1527            this.progressMonitor = progressMonitor;
     1528            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
     1529            if (this.tileLoader instanceof TMSCachedTileLoader) {
     1530                ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
     1531                        TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
     1532            }
     1533
     1534        }
     1535
     1536        /**
     1537         * @return true, if all is done
     1538         */
     1539        public boolean isFinished() {
     1540            return processedCount >= totalCount;
     1541        }
     1542
     1543        /**
     1544         * @return total number of tiles to download
     1545         */
     1546        public int getTotalCount() {
     1547            return totalCount;
     1548        }
     1549
     1550        /**
     1551         * cancel the task
     1552         */
     1553        public void cancel() {
     1554            if (tileLoader instanceof TMSCachedTileLoader) {
     1555                ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks();
     1556            }
     1557        }
     1558
     1559
     1560        @Override
     1561        public void tileLoadingFinished(Tile tile, boolean success) {
     1562            if (success) {
     1563                this.processedCount++;
     1564                this.progressMonitor.worked(1);
     1565                this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processedCount, totalCount));
     1566            }
     1567        }
     1568
     1569        /**
     1570         * @return tile loader that is used to load the tiles
     1571         */
     1572        public TileLoader getTileLoader() {
     1573            return tileLoader;
     1574        }
     1575    }
     1576
     1577    /**
     1578     * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
     1579     * all of the tiles. Buffer contains at least one tile.
     1580     *
     1581     * To prevent accidental clear of the queue, new download executor is created with separate queue
     1582     *
     1583     * @param precacheTask
     1584     * @param points
     1585     * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
     1586     * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
     1587     */
     1588    public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
     1589        final Set<Tile> requestedTiles = new ConcurrentSkipListSet <>(new Comparator<Tile>() {
     1590            public int compare(Tile o1, Tile o2) {
     1591                return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
     1592            }
     1593        });
     1594        for (LatLon point: points) {
     1595
     1596            TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
     1597            TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
     1598            TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
     1599
     1600            // take at least one tile of buffer
     1601            int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
     1602            int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
     1603            int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
     1604            int maxX = Math.min(curTile.getXIndex() + 1, minTile.getXIndex());
     1605
     1606            for (int x= minX; x<=maxX; x++) {
     1607                for (int y= minY; y<=maxY; y++) {
     1608                    requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1609                }
     1610            }
     1611        }
     1612
     1613        precacheTask.totalCount = requestedTiles.size();
     1614        precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
     1615
     1616        TileLoader loader = precacheTask.getTileLoader();
     1617        for (Tile t: requestedTiles) {
     1618            loader.createTileLoaderJob(t).submit();
     1619        }
     1620    }
     1621
    15721622    @Override
    1573     public final boolean isProjectionSupported(Projection proj) {
    1574         return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
     1623    public boolean isSavable() {
     1624        return true; // With WMSLayerExporter
    15751625    }
    15761626
    15771627    @Override
    1578     public final String nameSupportedProjections() {
    1579         return tr("EPSG:4326 and Mercator projection are supported");
     1628    public File createAndOpenSaveFileChooser() {
     1629        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    15801630    }
    15811631}
  • trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java

    r8513 r8526  
    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;
    6012import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
    6113import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
    6214import 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;
     15import org.openstreetmap.josm.data.imagery.CachedAttributionBingAerialTileSource;
     16import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
    6917import org.openstreetmap.josm.data.imagery.ImageryInfo;
    7018import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    7119import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    72 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     20import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
    7321import org.openstreetmap.josm.data.preferences.BooleanProperty;
    7422import org.openstreetmap.josm.data.preferences.IntegerProperty;
    75 import org.openstreetmap.josm.data.preferences.StringProperty;
    7623import 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;
     24
    9425
    9526/**
     
    10233 *
    10334 */
    104 public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
    105     public static final String PREFERENCE_PREFIX   = "imagery.tms";
     35public class TMSLayer extends AbstractTileSourceLayer {
     36    private static final String PREFERENCE_PREFIX   = "imagery.tms";
    10637
    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;
     38    /** minimum zoom level for TMS layer */
     39    public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", AbstractTileSourceLayer.PROP_MIN_ZOOM_LVL.get());
     40    /** maximum zoom level for TMS layer */
     41    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", AbstractTileSourceLayer.PROP_MAX_ZOOM_LVL.get());
     42    /** shall TMS layers be added to download dialog */
     43    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
    11144
    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;
    120     static {
    121         String defPath = null;
    122         try {
    123             defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
    124         } catch (SecurityException e) {
    125             Main.warn(e);
    126         }
    127         PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
    128     }
     45    /** loader factory responsible for loading tiles for this layer */
     46    public static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("TMS"){
    12947
    130     private final class ShowTileInfoAction extends AbstractAction {
    131         private ShowTileInfoAction() {
    132             super(tr("Show Tile Info"));
     48        @Override
     49        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
     50                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
     51            return new TMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
    13352        }
    13453
    135         private String getSizeString(int size) {
    136             return new StringBuilder().append(size).append("x").append(size).toString();
    137         }
     54    };
    13855
    139         private JTextField createTextField(String text) {
    140             JTextField ret = new JTextField(text);
    141             ret.setEditable(false);
    142             ret.setBorder(BorderFactory.createEmptyBorder());
    143             return ret;
    144         }
    145 
    146         @Override
    147         public void actionPerformed(ActionEvent ae) {
    148             if (clickedTile != null) {
    149                 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
    150                 JPanel panel = new JPanel(new GridBagLayout());
    151                 Rectangle displaySize = tileToRect(clickedTile);
    152                 String url = "";
    153                 try {
    154                     url = clickedTile.getUrl();
    155                 } catch (IOException e) {
    156                     // silence exceptions
    157                     if (Main.isTraceEnabled()) {
    158                         Main.trace(e.getMessage());
    159                     }
    160                 }
    161 
    162                 String[][] content = {
    163                         {"Tile name", clickedTile.getKey()},
    164                         {"Tile url", url},
    165                         {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
    166                         {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
    167                 };
    168 
    169                 for (String[] entry: content) {
    170                     panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
    171                     panel.add(GBC.glue(5, 0), GBC.std());
    172                     panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
    173                 }
    174 
    175                 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
    176                     panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
    177                     panel.add(GBC.glue(5, 0), GBC.std());
    178                     String value = e.getValue();
    179                     if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
    180                         value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
    181                     }
    182                     panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
    183                 }
    184                 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
    185                 ed.setContent(panel);
    186                 ed.showDialog();
    187             }
    188         }
     56    /**
     57     * Create a layer based on ImageryInfo
     58     * @param info description of the layer
     59     */
     60    public TMSLayer(ImageryInfo info) {
     61        super(info);
    18962    }
    19063
    19164    /**
    192      * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map
    193      *
    194      */
    195     public interface TileLoaderFactory {
    196         /**
    197          * @param listener object that will be notified, when tile has finished loading
    198          * @return TileLoader that will notify the listener
    199          */
    200         TileLoader makeTileLoader(TileLoaderListener listener);
    201 
    202         /**
    203          * @param listener object that will be notified, when tile has finished loading
    204          * @param headers HTTP headers that should be sent by TileLoader to tile server
    205          * @return TileLoader that will notify the listener
    206          */
    207         TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
    208     }
    209 
    210     protected TileCache tileCache;
    211     protected TileSource tileSource;
    212     protected TileLoader tileLoader;
    213 
    214 
    215     public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
    216         @Override
    217         public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
    218             Map<String, String> headers = new HashMap<>();
    219             headers.put("User-Agent", Version.getInstance().getFullAgentString());
    220             headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
    221             if (inputHeaders != null)
    222                 headers.putAll(inputHeaders);
    223 
    224             try {
    225                 return new TMSCachedTileLoader(listener, "TMS",
    226                         Main.pref.getInteger("socket.timeout.connect", 15) * 1000,
    227                         Main.pref.getInteger("socket.timeout.read", 30) * 1000,
    228                         headers,
    229                         PROP_TILECACHE_DIR.get());
    230             } catch (IOException e) {
    231                 Main.warn(e);
    232             }
    233             return null;
    234         }
    235 
    236         @Override
    237         public TileLoader makeTileLoader(TileLoaderListener listener) {
    238             return makeTileLoader(listener, null);
    239         }
    240     };
    241 
    242     /**
    24365     * Plugins that wish to set custom tile loader should call this method
     66     * @param newLoaderFactory that will be used to load tiles
    24467     */
    24568
    246     public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
    247         TMSLayer.loaderFactory = loaderFactory;
     69    public static void setTileLoaderFactory(TileLoaderFactory newLoaderFactory) {
     70        loaderFactory = newLoaderFactory;
    24871    }
    24972
    25073    @Override
    251     public synchronized void tileLoadingFinished(Tile tile, boolean success) {
    252         if (tile.hasError()) {
    253             success = false;
    254             tile.setImage(null);
     74    protected TileLoaderFactory getTileLoaderFactory() {
     75        return loaderFactory;
     76    }
     77
     78    @Override
     79    protected Map<String, String> getHeaders(TileSource tileSource) {
     80        if (tileSource instanceof TemplatedTMSTileSource) {
     81            return ((TemplatedTMSTileSource)tileSource).getHeaders();
    25582        }
    256         if (sharpenLevel != 0 && success) {
    257             tile.setImage(sharpenImage(tile.getImage()));
    258         }
    259         tile.setLoaded(success);
    260         needRedraw = true;
    261         if (Main.map != null) {
    262             Main.map.repaint(100);
    263         }
    264         if (Main.isDebugEnabled()) {
    265             Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
    266         }
     83        return null;
    26784    }
    26885
    26986    /**
    270      * Clears the tile cache.
     87     * Creates and returns a new TileSource instance depending on the {@link ImageryType}
     88     * of the passed ImageryInfo object.
    27189     *
    272      * If the current tileLoader is an instance of OsmTileLoader, a new
    273      * TmsTileClearController is created and passed to the according clearCache
    274      * method.
     90     * If no appropriate TileSource is found, null is returned.
     91     * Currently supported ImageryType are {@link ImageryType#TMS},
     92     * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
    27593     *
    276      * @param monitor not used in this implementation - as cache clear is instaneus
     94     *
     95     * @param info imagery info
     96     * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
     97     * @throws IllegalArgumentException if url from imagery info is null or invalid
    27798     */
    278     public void clearTileCache(ProgressMonitor monitor) {
    279         tileCache.clear();
    280         if (tileLoader instanceof CachedTileLoader) {
    281             ((CachedTileLoader) tileLoader).clearCache(tileSource);
    282         }
    283         redraw();
     99    @Override
     100    protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
     101        return getTileSourceStatic(info);
    284102    }
    285103
    286104    /**
    287      * Zoomlevel at which tiles is currently downloaded.
    288      * Initial zoom lvl is set to bestZoom
     105     * Adds a context menu to the mapView.
    289106     */
    290     public int currentZoomLevel;
    291107
    292     private Tile clickedTile;
    293     private boolean needRedraw;
    294     private JPopupMenu tileOptionMenu;
    295     private Tile showMetadataTile;
    296     private AttributionSupport attribution = new AttributionSupport();
    297     private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
    298 
    299     protected boolean autoZoom;
    300     protected boolean autoLoad;
    301     protected boolean showErrors;
    302 
    303     /**
    304      * Initiates a repaint of Main.map
    305      *
    306      * @see Main#map
    307      * @see MapFrame#repaint()
    308      */
    309     protected void redraw() {
    310         needRedraw = true;
    311         Main.map.repaint();
     108    @Override
     109    public final boolean isProjectionSupported(Projection proj) {
     110        return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
    312111    }
    313112
    314     protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
    315         if (maxZoomLvl > MAX_ZOOM) {
    316             maxZoomLvl = MAX_ZOOM;
    317         }
    318         if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
    319             maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
    320         }
    321         if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
    322             maxZoomLvl = ts.getMaxZoom();
    323         }
    324         return maxZoomLvl;
    325     }
    326 
    327     public static int getMaxZoomLvl(TileSource ts) {
    328         return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
    329     }
    330 
    331     public static void setMaxZoomLvl(int maxZoomLvl) {
    332         Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));
    333         PROP_MAX_ZOOM_LVL.put(newMaxZoom);
    334     }
    335 
    336     static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
    337         if (minZoomLvl < MIN_ZOOM) {
    338             /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
    339             minZoomLvl = MIN_ZOOM;
    340         }
    341         if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
    342             /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
    343             minZoomLvl = getMaxZoomLvl(ts);
    344         }
    345         if (ts != null && ts.getMinZoom() > minZoomLvl) {
    346             /*Main.debug("Increasing min. zoom level to match tile source");*/
    347             minZoomLvl = ts.getMinZoom();
    348         }
    349         return minZoomLvl;
    350     }
    351 
    352     public static int getMinZoomLvl(TileSource ts) {
    353         return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
    354     }
    355 
    356     public static void setMinZoomLvl(int minZoomLvl) {
    357         minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
    358         PROP_MIN_ZOOM_LVL.put(minZoomLvl);
    359     }
    360 
    361     private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
    362 
    363         public CachedAttributionBingAerialTileSource(ImageryInfo info) {
    364             super(info);
    365         }
    366 
    367         class BingAttributionData extends CacheCustomContent<IOException> {
    368 
    369             public BingAttributionData() {
    370                 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
    371             }
    372 
    373             @Override
    374             protected byte[] updateData() throws IOException {
    375                 URL u = getAttributionUrl();
    376                 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
    377                     String r = scanner.useDelimiter("\\A").next();
    378                     Main.info("Successfully loaded Bing attribution data.");
    379                     return r.getBytes("UTF-8");
    380                 }
    381             }
    382         }
    383 
    384         @Override
    385         protected Callable<List<Attribution>> getAttributionLoaderCallable() {
    386             return new Callable<List<Attribution>>() {
    387 
    388                 @Override
    389                 public List<Attribution> call() throws Exception {
    390                     BingAttributionData attributionLoader = new BingAttributionData();
    391                     int waitTimeSec = 1;
    392                     while (true) {
    393                         try {
    394                             String xml = attributionLoader.updateIfRequiredString();
    395                             return parseAttributionText(new InputSource(new StringReader(xml)));
    396                         } catch (IOException ex) {
    397                             Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
    398                             Thread.sleep(waitTimeSec * 1000L);
    399                             waitTimeSec *= 2;
    400                         }
    401                     }
    402                 }
    403             };
    404         }
     113    @Override
     114    public final String nameSupportedProjections() {
     115        return tr("EPSG:4326 and Mercator projection are supported");
    405116    }
    406117
     
    417128     * @throws IllegalArgumentException if url from imagery info is null or invalid
    418129     */
    419     public static TileSource getTileSource(ImageryInfo info) {
     130    public static TileSource getTileSourceStatic(ImageryInfo info) throws IllegalArgumentException {
    420131        if (info.getImageryType() == ImageryType.TMS) {
    421             checkUrl(info.getUrl());
     132            TemplatedTMSTileSource.checkUrl(info.getUrl());
    422133            TMSTileSource t = new TemplatedTMSTileSource(info);
    423134            info.setAttribution(t);
    424135            return t;
    425         } else if (info.getImageryType() == ImageryType.BING) {
     136        } else if (info.getImageryType() == ImageryType.BING)
    426137            return new CachedAttributionBingAerialTileSource(info);
    427         } else if (info.getImageryType() == ImageryType.SCANEX) {
     138        else if (info.getImageryType() == ImageryType.SCANEX) {
    428139            return new ScanexTileSource(info);
    429140        }
     
    431142    }
    432143
    433     /**
    434      * Checks validity of given URL.
    435      * @param url URL to check
    436      * @throws IllegalArgumentException if url is null or invalid
    437      */
    438     public static void checkUrl(String url) {
    439         CheckParameterUtil.ensureParameterNotNull(url, "url");
    440         Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
    441         while (m.find()) {
    442             boolean isSupportedPattern = false;
    443             for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
    444                 if (m.group().matches(pattern)) {
    445                     isSupportedPattern = true;
    446                     break;
    447                 }
    448             }
    449             if (!isSupportedPattern) {
    450                 throw new IllegalArgumentException(
    451                         tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
    452             }
    453         }
    454     }
    455144
    456     private void initTileSource(TileSource tileSource) {
    457         this.tileSource = tileSource;
    458         attribution.initialize(tileSource);
    459145
    460         currentZoomLevel = getBestZoom();
    461 
    462         Map<String, String> headers = null;
    463         if (tileSource instanceof TemplatedTMSTileSource) {
    464             headers = (((TemplatedTMSTileSource) tileSource).getHeaders());
    465         }
    466 
    467         tileLoader = loaderFactory.makeTileLoader(this, headers);
    468         if (tileLoader instanceof TMSCachedTileLoader) {
    469             tileCache = (TileCache) tileLoader;
    470         } else {
    471             tileCache = new MemoryTileCache();
    472         }
    473         if (tileLoader == null)
    474             tileLoader = new OsmTileLoader(this);
    475     }
    476 
    477     /**
    478      * Marks layer as needing redraw on offset change
    479      */
    480     @Override
    481     public void setOffset(double dx, double dy) {
    482         super.setOffset(dx, dy);
    483         needRedraw = true;
    484     }
    485     /**
    486      * Returns average number of screen pixels per tile pixel for current mapview
    487      */
    488     private double getScaleFactor(int zoom) {
    489         if (!Main.isDisplayingMapView()) return 1;
    490         MapView mv = Main.map.mapView;
    491         LatLon topLeft = mv.getLatLon(0, 0);
    492         LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
    493         double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
    494         double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
    495         double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
    496         double y2 = tileSource.latToTileY(botRight.lat(), zoom);
    497 
    498         int screenPixels = mv.getWidth()*mv.getHeight();
    499         double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
    500         if (screenPixels == 0 || tilePixels == 0) return 1;
    501         return screenPixels/tilePixels;
    502     }
    503 
    504     private int getBestZoom() {
    505         double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
    506         double result = Math.log(factor)/Math.log(2)/2+1;
    507         /*
    508          * Math.log(factor)/Math.log(2) - gives log base 2 of factor
    509          * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
    510          * In general, smaller zoom levels are more readable.  We prefer big,
    511          * block, pixelated (but readable) map text to small, smeared,
    512          * unreadable underzoomed text.  So, use .floor() instead of rounding
    513          * to skew things a bit toward the lower zooms.
    514          * Remember, that result here, should correspond to TMSLayer.paint(...)
    515          * getScaleFactor(...) is supposed to be between 0.75 and 3
    516          */
    517         int intResult = (int) Math.floor(result);
    518         if (intResult > getMaxZoomLvl())
    519             return getMaxZoomLvl();
    520         if (intResult < getMinZoomLvl())
    521             return getMinZoomLvl();
    522         return intResult;
    523     }
    524 
    525     @SuppressWarnings("serial")
    526     public TMSLayer(ImageryInfo info) {
    527         super(info);
    528 
    529         if (!isProjectionSupported(Main.getProjection())) {
    530             JOptionPane.showMessageDialog(Main.parent,
    531                     tr("TMS layers do not support the projection {0}.\n{1}\n"
    532                             + "Change the projection or remove the layer.",
    533                             Main.getProjection().toCode(), nameSupportedProjections()),
    534                             tr("Warning"),
    535                             JOptionPane.WARNING_MESSAGE);
    536         }
    537 
    538         setBackgroundLayer(true);
    539         this.setVisible(true);
    540 
    541         TileSource source = getTileSource(info);
    542         if (source == null)
    543             throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
    544         initTileSource(source);
    545 
    546         MapView.addZoomChangeListener(this);
    547     }
    548 
    549     /**
    550      * Adds a context menu to the mapView.
    551      */
    552     @Override
    553     public void hookUpMapView() {
    554         tileOptionMenu = new JPopupMenu();
    555 
    556         autoZoom = PROP_DEFAULT_AUTOZOOM.get();
    557         JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
    558         autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
    559             @Override
    560             public void actionPerformed(ActionEvent ae) {
    561                 autoZoom = !autoZoom;
    562             }
    563         });
    564         autoZoomPopup.setSelected(autoZoom);
    565         tileOptionMenu.add(autoZoomPopup);
    566 
    567         autoLoad = PROP_DEFAULT_AUTOLOAD.get();
    568         JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
    569         autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
    570             @Override
    571             public void actionPerformed(ActionEvent ae) {
    572                 autoLoad = !autoLoad;
    573             }
    574         });
    575         autoLoadPopup.setSelected(autoLoad);
    576         tileOptionMenu.add(autoLoadPopup);
    577 
    578         showErrors = PROP_DEFAULT_SHOWERRORS.get();
    579         JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
    580         showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
    581             @Override
    582             public void actionPerformed(ActionEvent ae) {
    583                 showErrors = !showErrors;
    584             }
    585         });
    586         showErrorsPopup.setSelected(showErrors);
    587         tileOptionMenu.add(showErrorsPopup);
    588 
    589         tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
    590             @Override
    591             public void actionPerformed(ActionEvent ae) {
    592                 if (clickedTile != null) {
    593                     loadTile(clickedTile, true);
    594                     redraw();
    595                 }
    596             }
    597         }));
    598 
    599         tileOptionMenu.add(new JMenuItem(new ShowTileInfoAction()));
    600 
    601         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    602                 tr("Request Update")) {
    603             @Override
    604             public void actionPerformed(ActionEvent ae) {
    605                 if (clickedTile != null) {
    606                     clickedTile.setLoaded(false);
    607                     tileLoader.createTileLoaderJob(clickedTile).submit(true);
    608                 }
    609             }
    610         }));
    611 
    612         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    613                 tr("Load All Tiles")) {
    614             @Override
    615             public void actionPerformed(ActionEvent ae) {
    616                 loadAllTiles(true);
    617                 redraw();
    618             }
    619         }));
    620 
    621         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    622                 tr("Load All Error Tiles")) {
    623             @Override
    624             public void actionPerformed(ActionEvent ae) {
    625                 loadAllErrorTiles(true);
    626                 redraw();
    627             }
    628         }));
    629 
    630         // increase and decrease commands
    631         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    632                 tr("Increase zoom")) {
    633             @Override
    634             public void actionPerformed(ActionEvent ae) {
    635                 increaseZoomLevel();
    636                 redraw();
    637             }
    638         }));
    639 
    640         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    641                 tr("Decrease zoom")) {
    642             @Override
    643             public void actionPerformed(ActionEvent ae) {
    644                 decreaseZoomLevel();
    645                 redraw();
    646             }
    647         }));
    648 
    649         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    650                 tr("Snap to tile size")) {
    651             @Override
    652             public void actionPerformed(ActionEvent ae) {
    653                 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
    654                 Main.map.mapView.zoomToFactor(newFactor);
    655                 redraw();
    656             }
    657         }));
    658 
    659         tileOptionMenu.add(new JMenuItem(new AbstractAction(
    660                 tr("Flush Tile Cache")) {
    661             @Override
    662             public void actionPerformed(ActionEvent ae) {
    663                 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
    664                     @Override
    665                     protected void realRun() throws SAXException, IOException,
    666                             OsmTransferException {
    667                         clearTileCache(getProgressMonitor());
    668                     }
    669 
    670                     @Override
    671                     protected void finish() {
    672                     }
    673 
    674                     @Override
    675                     protected void cancel() {
    676                     }
    677                 }.run();
    678             }
    679         }));
    680 
    681         final MouseAdapter adapter = new MouseAdapter() {
    682             @Override
    683             public void mouseClicked(MouseEvent e) {
    684                 if (!isVisible()) return;
    685                 if (e.getButton() == MouseEvent.BUTTON3) {
    686                     clickedTile = getTileForPixelpos(e.getX(), e.getY());
    687                     tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
    688                 } else if (e.getButton() == MouseEvent.BUTTON1) {
    689                     attribution.handleAttribution(e.getPoint(), true);
    690                 }
    691             }
    692         };
    693         Main.map.mapView.addMouseListener(adapter);
    694 
    695         MapView.addLayerChangeListener(new LayerChangeListener() {
    696             @Override
    697             public void activeLayerChange(Layer oldLayer, Layer newLayer) {
    698                 //
    699             }
    700 
    701             @Override
    702             public void layerAdded(Layer newLayer) {
    703                 //
    704             }
    705 
    706             @Override
    707             public void layerRemoved(Layer oldLayer) {
    708                 if (oldLayer == TMSLayer.this) {
    709                     Main.map.mapView.removeMouseListener(adapter);
    710                     MapView.removeZoomChangeListener(TMSLayer.this);
    711                     MapView.removeLayerChangeListener(this);
    712                 }
    713             }
    714         });
    715     }
    716 
    717     /**
    718      * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
    719      * changes to visible map (panning/zooming)
    720      */
    721     @Override
    722     public void zoomChanged() {
    723         if (Main.isDebugEnabled()) {
    724             Main.debug("zoomChanged(): " + currentZoomLevel);
    725         }
    726         needRedraw = true;
    727         if (tileLoader instanceof TMSCachedTileLoader) {
    728             ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
    729         }
    730     }
    731 
    732     protected int getMaxZoomLvl() {
    733         if (info.getMaxZoom() != 0)
    734             return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
    735         else
    736             return getMaxZoomLvl(tileSource);
    737     }
    738 
    739     protected int getMinZoomLvl() {
    740         return getMinZoomLvl(tileSource);
    741     }
    742 
    743     /**
    744      * Zoom in, go closer to map.
    745      *
    746      * @return    true, if zoom increasing was successful, false otherwise
    747      */
    748     public boolean zoomIncreaseAllowed() {
    749         boolean zia = currentZoomLevel < this.getMaxZoomLvl();
    750         if (Main.isDebugEnabled()) {
    751             Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
    752         }
    753         return zia;
    754     }
    755 
    756     public boolean increaseZoomLevel() {
    757         if (zoomIncreaseAllowed()) {
    758             currentZoomLevel++;
    759             if (Main.isDebugEnabled()) {
    760                 Main.debug("increasing zoom level to: " + currentZoomLevel);
    761             }
    762             zoomChanged();
    763         } else {
    764             Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
    765                     "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
    766             return false;
    767         }
    768         return true;
    769     }
    770 
    771     public boolean setZoomLevel(int zoom) {
    772         if (zoom == currentZoomLevel) return true;
    773         if (zoom > this.getMaxZoomLvl()) return false;
    774         if (zoom < this.getMinZoomLvl()) return false;
    775         currentZoomLevel = zoom;
    776         zoomChanged();
    777         return true;
    778     }
    779 
    780     /**
    781      * Check if zooming out is allowed
    782      *
    783      * @return    true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
    784      */
    785     public boolean zoomDecreaseAllowed() {
    786         return currentZoomLevel > this.getMinZoomLvl();
    787     }
    788 
    789     /**
    790      * Zoom out from map.
    791      *
    792      * @return    true, if zoom increasing was successfull, false othervise
    793      */
    794     public boolean decreaseZoomLevel() {
    795         //int minZoom = this.getMinZoomLvl();
    796         if (zoomDecreaseAllowed()) {
    797             if (Main.isDebugEnabled()) {
    798                 Main.debug("decreasing zoom level to: " + currentZoomLevel);
    799             }
    800             currentZoomLevel--;
    801             zoomChanged();
    802         } else {
    803             /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
    804             return false;
    805         }
    806         return true;
    807     }
    808 
    809     /*
    810      * We use these for quick, hackish calculations.  They
    811      * are temporary only and intentionally not inserted
    812      * into the tileCache.
    813      */
    814     private Tile tempCornerTile(Tile t) {
    815         int x = t.getXtile() + 1;
    816         int y = t.getYtile() + 1;
    817         int zoom = t.getZoom();
    818         Tile tile = getTile(x, y, zoom);
    819         if (tile != null)
    820             return tile;
    821         return new Tile(tileSource, x, y, zoom);
    822     }
    823 
    824     private Tile getOrCreateTile(int x, int y, int zoom) {
    825         Tile tile = getTile(x, y, zoom);
    826         if (tile == null) {
    827             tile = new Tile(tileSource, x, y, zoom);
    828             tileCache.addTile(tile);
    829             tile.loadPlaceholderFromCache(tileCache);
    830         }
    831         return tile;
    832     }
    833 
    834     /*
    835      * This can and will return null for tiles that are not
    836      * already in the cache.
    837      */
    838     private Tile getTile(int x, int y, int zoom) {
    839         int max = 1 << zoom;
    840         if (x < 0 || x >= max || y < 0 || y >= max)
    841             return null;
    842         return tileCache.getTile(tileSource, x, y, zoom);
    843     }
    844 
    845     private boolean loadTile(Tile tile, boolean force) {
    846         if (tile == null)
    847             return false;
    848         if (!force && (tile.isLoaded() || tile.hasError()))
    849             return false;
    850         if (tile.isLoading())
    851             return false;
    852         tileLoader.createTileLoaderJob(tile).submit(force);
    853         return true;
    854     }
    855 
    856     private void loadAllTiles(boolean force) {
    857         MapView mv = Main.map.mapView;
    858         EastNorth topLeft = mv.getEastNorth(0, 0);
    859         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    860 
    861         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
    862 
    863         // if there is more than 18 tiles on screen in any direction, do not
    864         // load all tiles!
    865         if (ts.tooLarge()) {
    866             Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
    867             return;
    868         }
    869         ts.loadAllTiles(force);
    870     }
    871 
    872     private void loadAllErrorTiles(boolean force) {
    873         MapView mv = Main.map.mapView;
    874         EastNorth topLeft = mv.getEastNorth(0, 0);
    875         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    876 
    877         TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
    878 
    879         ts.loadAllErrorTiles(force);
    880     }
    881 
    882     @Override
    883     public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    884         boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
    885         needRedraw = true;
    886         if (Main.isDebugEnabled()) {
    887             Main.debug("imageUpdate() done: " + done + " calling repaint");
    888         }
    889         Main.map.repaint(done ? 0 : 100);
    890         return !done;
    891     }
    892 
    893     private boolean imageLoaded(Image i) {
    894         if (i == null)
    895             return false;
    896         int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
    897         if ((status & ALLBITS) != 0)
    898             return true;
    899         return false;
    900     }
    901 
    902     /**
    903      * Returns the image for the given tile if both tile and image are loaded.
    904      * Otherwise returns  null.
    905      *
    906      * @param tile the Tile for which the image should be returned
    907      * @return  the image of the tile or null.
    908      */
    909     private Image getLoadedTileImage(Tile tile) {
    910         if (!tile.isLoaded())
    911             return null;
    912         Image img = tile.getImage();
    913         if (!imageLoaded(img))
    914             return null;
    915         return img;
    916     }
    917 
    918     private LatLon tileLatLon(Tile t) {
    919         int zoom = t.getZoom();
    920         return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
    921                 tileSource.tileXToLon(t.getXtile(), zoom));
    922     }
    923 
    924     private Rectangle tileToRect(Tile t1) {
    925         /*
    926          * We need to get a box in which to draw, so advance by one tile in
    927          * each direction to find the other corner of the box.
    928          * Note: this somewhat pollutes the tile cache
    929          */
    930         Tile t2 = tempCornerTile(t1);
    931         Rectangle rect = new Rectangle(pixelPos(t1));
    932         rect.add(pixelPos(t2));
    933         return rect;
    934     }
    935 
    936     // 'source' is the pixel coordinates for the area that
    937     // the img is capable of filling in.  However, we probably
    938     // only want a portion of it.
    939     //
    940     // 'border' is the screen cordinates that need to be drawn.
    941     //  We must not draw outside of it.
    942     private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
    943         Rectangle target = source;
    944 
    945         // If a border is specified, only draw the intersection
    946         // if what we have combined with what we are supposed
    947         // to draw.
    948         if (border != null) {
    949             target = source.intersection(border);
    950             if (Main.isDebugEnabled()) {
    951                 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
    952             }
    953         }
    954 
    955         // All of the rectangles are in screen coordinates.  We need
    956         // to how these correlate to the sourceImg pixels.  We could
    957         // avoid doing this by scaling the image up to the 'source' size,
    958         // but this should be cheaper.
    959         //
    960         // In some projections, x any y are scaled differently enough to
    961         // cause a pixel or two of fudge.  Calculate them separately.
    962         double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
    963         double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
    964 
    965         // How many pixels into the 'source' rectangle are we drawing?
    966         int screen_x_offset = target.x - source.x;
    967         int screen_y_offset = target.y - source.y;
    968         // And how many pixels into the image itself does that
    969         // correlate to?
    970         int img_x_offset = (int) (screen_x_offset * imageXScaling + 0.5);
    971         int img_y_offset = (int) (screen_y_offset * imageYScaling + 0.5);
    972         // Now calculate the other corner of the image that we need
    973         // by scaling the 'target' rectangle's dimensions.
    974         int img_x_end = img_x_offset + (int) (target.getWidth() * imageXScaling + 0.5);
    975         int img_y_end = img_y_offset + (int) (target.getHeight() * imageYScaling + 0.5);
    976 
    977         if (Main.isDebugEnabled()) {
    978             Main.debug("drawing image into target rect: " + target);
    979         }
    980         g.drawImage(sourceImg,
    981                 target.x, target.y,
    982                 target.x + target.width, target.y + target.height,
    983                 img_x_offset, img_y_offset,
    984                 img_x_end, img_y_end,
    985                 this);
    986         if (PROP_FADE_AMOUNT.get() != 0) {
    987             // dimm by painting opaque rect...
    988             g.setColor(getFadeColorWithAlpha());
    989             g.fillRect(target.x, target.y,
    990                     target.width, target.height);
    991         }
    992     }
    993 
    994     // This function is called for several zoom levels, not just
    995     // the current one.  It should not trigger any tiles to be
    996     // downloaded.  It should also avoid polluting the tile cache
    997     // with any tiles since these tiles are not mandatory.
    998     //
    999     // The "border" tile tells us the boundaries of where we may
    1000     // draw.  It will not be from the zoom level that is being
    1001     // drawn currently.  If drawing the displayZoomLevel,
    1002     // border is null and we draw the entire tile set.
    1003     private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
    1004         if (zoom <= 0) return Collections.emptyList();
    1005         Rectangle borderRect = null;
    1006         if (border != null) {
    1007             borderRect = tileToRect(border);
    1008         }
    1009         List<Tile> missedTiles = new LinkedList<>();
    1010         // The callers of this code *require* that we return any tiles
    1011         // that we do not draw in missedTiles.  ts.allExistingTiles() by
    1012         // default will only return already-existing tiles.  However, we
    1013         // need to return *all* tiles to the callers, so force creation
    1014         // here.
    1015         //boolean forceTileCreation = true;
    1016         for (Tile tile : ts.allTilesCreate()) {
    1017             Image img = getLoadedTileImage(tile);
    1018             if (img == null || tile.hasError()) {
    1019                 if (Main.isDebugEnabled()) {
    1020                     Main.debug("missed tile: " + tile);
    1021                 }
    1022                 missedTiles.add(tile);
    1023                 continue;
    1024             }
    1025             Rectangle sourceRect = tileToRect(tile);
    1026             if (borderRect != null && !sourceRect.intersects(borderRect)) {
    1027                 continue;
    1028             }
    1029             drawImageInside(g, img, sourceRect, borderRect);
    1030         }
    1031         return missedTiles;
    1032     }
    1033 
    1034     private void myDrawString(Graphics g, String text, int x, int y) {
    1035         Color oldColor = g.getColor();
    1036         g.setColor(Color.black);
    1037         g.drawString(text, x+1, y+1);
    1038         g.setColor(oldColor);
    1039         g.drawString(text, x, y);
    1040     }
    1041 
    1042     private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
    1043         int fontHeight = g.getFontMetrics().getHeight();
    1044         if (tile == null)
    1045             return;
    1046         Point p = pixelPos(t);
    1047         int texty = p.y + 2 + fontHeight;
    1048 
    1049         /*if (PROP_DRAW_DEBUG.get()) {
    1050             myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
    1051             texty += 1 + fontHeight;
    1052             if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
    1053                 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
    1054                 texty += 1 + fontHeight;
    1055             }
    1056         }*/
    1057 
    1058         if (tile == showMetadataTile) {
    1059             String md = tile.toString();
    1060             if (md != null) {
    1061                 myDrawString(g, md, p.x + 2, texty);
    1062                 texty += 1 + fontHeight;
    1063             }
    1064             Map<String, String> meta = tile.getMetadata();
    1065             if (meta != null) {
    1066                 for (Map.Entry<String, String> entry : meta.entrySet()) {
    1067                     myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
    1068                     texty += 1 + fontHeight;
    1069                 }
    1070             }
    1071         }
    1072 
    1073         /*String tileStatus = tile.getStatus();
    1074         if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
    1075             myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
    1076             texty += 1 + fontHeight;
    1077         }*/
    1078 
    1079         if (tile.hasError() && showErrors) {
    1080             myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
    1081             texty += 1 + fontHeight;
    1082         }
    1083 
    1084         /*int xCursor = -1;
    1085         int yCursor = -1;
    1086         if (PROP_DRAW_DEBUG.get()) {
    1087             if (yCursor < t.getYtile()) {
    1088                 if (t.getYtile() % 32 == 31) {
    1089                     g.fillRect(0, p.y - 1, mv.getWidth(), 3);
    1090                 } else {
    1091                     g.drawLine(0, p.y, mv.getWidth(), p.y);
    1092                 }
    1093                 yCursor = t.getYtile();
    1094             }
    1095             // This draws the vertical lines for the entire
    1096             // column. Only draw them for the top tile in
    1097             // the column.
    1098             if (xCursor < t.getXtile()) {
    1099                 if (t.getXtile() % 32 == 0) {
    1100                     // level 7 tile boundary
    1101                     g.fillRect(p.x - 1, 0, 3, mv.getHeight());
    1102                 } else {
    1103                     g.drawLine(p.x, 0, p.x, mv.getHeight());
    1104                 }
    1105                 xCursor = t.getXtile();
    1106             }
    1107         }*/
    1108     }
    1109 
    1110     private Point pixelPos(LatLon ll) {
    1111         return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
    1112     }
    1113 
    1114     private Point pixelPos(Tile t) {
    1115         double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
    1116         LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
    1117         return pixelPos(tmpLL);
    1118     }
    1119 
    1120     private LatLon getShiftedLatLon(EastNorth en) {
    1121         return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
    1122     }
    1123 
    1124     private Coordinate getShiftedCoord(EastNorth en) {
    1125         LatLon ll = getShiftedLatLon(en);
    1126         return new Coordinate(ll.lat(), ll.lon());
    1127     }
    1128 
    1129     private final TileSet nullTileSet = new TileSet((LatLon) null, (LatLon) null, 0);
    1130 
    1131     private final class TileSet {
    1132         private int x0, x1, y0, y1;
    1133         private int zoom;
    1134         private int tileMax = -1;
    1135 
    1136         /**
    1137          * Create a TileSet by EastNorth bbox taking a layer shift in account
    1138          */
    1139         private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
    1140             this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
    1141         }
    1142 
    1143         /**
    1144          * Create a TileSet by known LatLon bbox without layer shift correction
    1145          */
    1146         private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
    1147             this.zoom = zoom;
    1148             if (zoom == 0)
    1149                 return;
    1150 
    1151             x0 = (int) tileSource.lonToTileX(topLeft.lon(),  zoom);
    1152             y0 = (int) tileSource.latToTileY(topLeft.lat(),  zoom);
    1153             x1 = (int) tileSource.lonToTileX(botRight.lon(), zoom);
    1154             y1 = (int) tileSource.latToTileY(botRight.lat(), zoom);
    1155             if (x0 > x1) {
    1156                 int tmp = x0;
    1157                 x0 = x1;
    1158                 x1 = tmp;
    1159             }
    1160             if (y0 > y1) {
    1161                 int tmp = y0;
    1162                 y0 = y1;
    1163                 y1 = tmp;
    1164             }
    1165             tileMax = (int) Math.pow(2.0, zoom);
    1166             if (x0 < 0) {
    1167                 x0 = 0;
    1168             }
    1169             if (y0 < 0) {
    1170                 y0 = 0;
    1171             }
    1172             if (x1 > tileMax) {
    1173                 x1 = tileMax;
    1174             }
    1175             if (y1 > tileMax) {
    1176                 y1 = tileMax;
    1177             }
    1178         }
    1179 
    1180         private boolean tooSmall() {
    1181             return this.tilesSpanned() < 2.1;
    1182         }
    1183 
    1184         private boolean tooLarge() {
    1185             return this.tilesSpanned() > 10;
    1186         }
    1187 
    1188         private boolean insane() {
    1189             return this.tilesSpanned() > 100;
    1190         }
    1191 
    1192         private double tilesSpanned() {
    1193             return Math.sqrt(1.0 * this.size());
    1194         }
    1195 
    1196         private int size() {
    1197             int x_span = x1 - x0 + 1;
    1198             int y_span = y1 - y0 + 1;
    1199             return x_span * y_span;
    1200         }
    1201 
    1202         /*
    1203          * Get all tiles represented by this TileSet that are
    1204          * already in the tileCache.
    1205          */
    1206         private List<Tile> allExistingTiles() {
    1207             return this.__allTiles(false);
    1208         }
    1209 
    1210         private List<Tile> allTilesCreate() {
    1211             return this.__allTiles(true);
    1212         }
    1213 
    1214         private List<Tile> __allTiles(boolean create) {
    1215             // Tileset is either empty or too large
    1216             if (zoom == 0 || this.insane())
    1217                 return Collections.emptyList();
    1218             List<Tile> ret = new ArrayList<>();
    1219             for (int x = x0; x <= x1; x++) {
    1220                 for (int y = y0; y <= y1; y++) {
    1221                     Tile t;
    1222                     if (create) {
    1223                         t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
    1224                     } else {
    1225                         t = getTile(x % tileMax, y % tileMax, zoom);
    1226                     }
    1227                     if (t != null) {
    1228                         ret.add(t);
    1229                     }
    1230                 }
    1231             }
    1232             return ret;
    1233         }
    1234 
    1235         private List<Tile> allLoadedTiles() {
    1236             List<Tile> ret = new ArrayList<>();
    1237             for (Tile t : this.allExistingTiles()) {
    1238                 if (t.isLoaded())
    1239                     ret.add(t);
    1240             }
    1241             return ret;
    1242         }
    1243 
    1244         private Comparator<Tile> getTileDistanceComparator() {
    1245             final int centerX = (int) Math.ceil((x0 + x1) / 2);
    1246             final int centerY = (int) Math.ceil((y0 + y1) / 2);
    1247             return new Comparator<Tile>() {
    1248                 private int getDistance(Tile t) {
    1249                     return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
    1250                 }
    1251 
    1252                 @Override
    1253                 public int compare(Tile o1, Tile o2) {
    1254                     int distance1 = getDistance(o1);
    1255                     int distance2 = getDistance(o2);
    1256                     return Integer.compare(distance1, distance2);
    1257                 }
    1258             };
    1259         }
    1260 
    1261         private void loadAllTiles(boolean force) {
    1262             if (!autoLoad && !force)
    1263                 return;
    1264             List<Tile> allTiles = allTilesCreate();
    1265             Collections.sort(allTiles, getTileDistanceComparator());
    1266             for (Tile t : allTiles) {
    1267                 loadTile(t, force);
    1268             }
    1269         }
    1270 
    1271         private void loadAllErrorTiles(boolean force) {
    1272             if (!autoLoad && !force)
    1273                 return;
    1274             for (Tile t : this.allTilesCreate()) {
    1275                 if (t.hasError()) {
    1276                     loadTile(t, true);
    1277                 }
    1278             }
    1279         }
    1280     }
    1281 
    1282     private static class TileSetInfo {
    1283         public boolean hasVisibleTiles = false;
    1284         public boolean hasOverzoomedTiles = false;
    1285         public boolean hasLoadingTiles = false;
    1286     }
    1287 
    1288     private static TileSetInfo getTileSetInfo(TileSet ts) {
    1289         List<Tile> allTiles = ts.allExistingTiles();
    1290         TileSetInfo result = new TileSetInfo();
    1291         result.hasLoadingTiles = allTiles.size() < ts.size();
    1292         for (Tile t : allTiles) {
    1293             if (t.isLoaded()) {
    1294                 if (!t.hasError()) {
    1295                     result.hasVisibleTiles = true;
    1296                 }
    1297                 if ("no-tile".equals(t.getValue("tile-info"))) {
    1298                     result.hasOverzoomedTiles = true;
    1299                 }
    1300             } else {
    1301                 result.hasLoadingTiles = true;
    1302             }
    1303         }
    1304         return result;
    1305     }
    1306 
    1307     private class DeepTileSet {
    1308         private final EastNorth topLeft, botRight;
    1309         private final int minZoom, maxZoom;
    1310         private final TileSet[] tileSets;
    1311         private final TileSetInfo[] tileSetInfos;
    1312 
    1313         public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
    1314             this.topLeft = topLeft;
    1315             this.botRight = botRight;
    1316             this.minZoom = minZoom;
    1317             this.maxZoom = maxZoom;
    1318             this.tileSets = new TileSet[maxZoom - minZoom + 1];
    1319             this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
    1320         }
    1321 
    1322         public TileSet getTileSet(int zoom) {
    1323             if (zoom < minZoom)
    1324                 return nullTileSet;
    1325             synchronized (tileSets) {
    1326                 TileSet ts = tileSets[zoom-minZoom];
    1327                 if (ts == null) {
    1328                     ts = new TileSet(topLeft, botRight, zoom);
    1329                     tileSets[zoom-minZoom] = ts;
    1330                 }
    1331                 return ts;
    1332             }
    1333         }
    1334 
    1335         public TileSetInfo getTileSetInfo(int zoom) {
    1336             if (zoom < minZoom)
    1337                 return new TileSetInfo();
    1338             synchronized (tileSetInfos) {
    1339                 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    1340                 if (tsi == null) {
    1341                     tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
    1342                     tileSetInfos[zoom-minZoom] = tsi;
    1343                 }
    1344                 return tsi;
    1345             }
    1346         }
    1347     }
    1348 
    1349     @Override
    1350     public void paint(Graphics2D g, MapView mv, Bounds bounds) {
    1351         EastNorth topLeft = mv.getEastNorth(0, 0);
    1352         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    1353 
    1354         if (botRight.east() == 0 || botRight.north() == 0) {
    1355             /*Main.debug("still initializing??");*/
    1356             // probably still initializing
    1357             return;
    1358         }
    1359 
    1360         needRedraw = false;
    1361 
    1362         int zoom = currentZoomLevel;
    1363         if (autoZoom) {
    1364             double pixelScaling = getScaleFactor(zoom);
    1365             if (pixelScaling > 3 || pixelScaling < 0.7) {
    1366                 zoom = getBestZoom();
    1367             }
    1368         }
    1369 
    1370         DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
    1371         TileSet ts = dts.getTileSet(zoom);
    1372 
    1373         int displayZoomLevel = zoom;
    1374 
    1375         boolean noTilesAtZoom = false;
    1376         if (autoZoom && autoLoad) {
    1377             // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
    1378             TileSetInfo tsi = dts.getTileSetInfo(zoom);
    1379             if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
    1380                 noTilesAtZoom = true;
    1381             }
    1382             // Find highest zoom level with at least one visible tile
    1383             for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
    1384                 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
    1385                     displayZoomLevel = tmpZoom;
    1386                     break;
    1387                 }
    1388             }
    1389             // Do binary search between currentZoomLevel and displayZoomLevel
    1390             while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
    1391                 zoom = (zoom + displayZoomLevel)/2;
    1392                 tsi = dts.getTileSetInfo(zoom);
    1393             }
    1394 
    1395             setZoomLevel(zoom);
    1396 
    1397             // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
    1398             // to make sure there're really no more zoom levels
    1399             if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
    1400                 zoom++;
    1401                 tsi = dts.getTileSetInfo(zoom);
    1402             }
    1403             // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
    1404             // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
    1405             while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
    1406                 zoom--;
    1407                 tsi = dts.getTileSetInfo(zoom);
    1408             }
    1409             ts = dts.getTileSet(zoom);
    1410         } else if (autoZoom) {
    1411             setZoomLevel(zoom);
    1412         }
    1413 
    1414         // Too many tiles... refuse to download
    1415         if (!ts.tooLarge()) {
    1416             //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
    1417             ts.loadAllTiles(false);
    1418         }
    1419 
    1420         if (displayZoomLevel != zoom) {
    1421             ts = dts.getTileSet(displayZoomLevel);
    1422         }
    1423 
    1424         g.setColor(Color.DARK_GRAY);
    1425 
    1426         List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
    1427         int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
    1428         for (int zoomOffset : otherZooms) {
    1429             if (!autoZoom) {
    1430                 break;
    1431             }
    1432             int newzoom = displayZoomLevel + zoomOffset;
    1433             if (newzoom < MIN_ZOOM) {
    1434                 continue;
    1435             }
    1436             if (missedTiles.isEmpty()) {
    1437                 break;
    1438             }
    1439             List<Tile> newlyMissedTiles = new LinkedList<>();
    1440             for (Tile missed : missedTiles) {
    1441                 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
    1442                     // Don't try to paint from higher zoom levels when tile is overzoomed
    1443                     newlyMissedTiles.add(missed);
    1444                     continue;
    1445                 }
    1446                 Tile t2 = tempCornerTile(missed);
    1447                 LatLon topLeft2  = tileLatLon(missed);
    1448                 LatLon botRight2 = tileLatLon(t2);
    1449                 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
    1450                 // Instantiating large TileSets is expensive.  If there
    1451                 // are no loaded tiles, don't bother even trying.
    1452                 if (ts2.allLoadedTiles().isEmpty()) {
    1453                     newlyMissedTiles.add(missed);
    1454                     continue;
    1455                 }
    1456                 if (ts2.tooLarge()) {
    1457                     continue;
    1458                 }
    1459                 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
    1460             }
    1461             missedTiles = newlyMissedTiles;
    1462         }
    1463         if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
    1464             Main.debug("still missed "+missedTiles.size()+" in the end");
    1465         }
    1466         g.setColor(Color.red);
    1467         g.setFont(InfoFont);
    1468 
    1469         // The current zoom tileset should have all of its tiles
    1470         // due to the loadAllTiles(), unless it to tooLarge()
    1471         for (Tile t : ts.allExistingTiles()) {
    1472             this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
    1473         }
    1474 
    1475         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
    1476 
    1477         //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
    1478         g.setColor(Color.lightGray);
    1479         if (!autoZoom) {
    1480             if (ts.insane()) {
    1481                 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
    1482             } else if (ts.tooLarge()) {
    1483                 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
    1484             } else if (ts.tooSmall()) {
    1485                 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
    1486             }
    1487         }
    1488         if (noTilesAtZoom) {
    1489             myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
    1490         }
    1491         if (Main.isDebugEnabled()) {
    1492             myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
    1493             myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
    1494             myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
    1495             myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
    1496             if (tileLoader instanceof TMSCachedTileLoader) {
    1497                 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
    1498                 int offset = 185;
    1499                 for (String part: cachedTileLoader.getStats().split("\n")) {
    1500                     myDrawString(g, tr("Cache stats: {0}", part), 50, offset += 15);
    1501                 }
    1502             }
    1503         }
    1504     }
    1505 
    1506     /**
    1507      * This isn't very efficient, but it is only used when the
    1508      * user right-clicks on the map.
    1509      */
    1510     private Tile getTileForPixelpos(int px, int py) {
    1511         if (Main.isDebugEnabled()) {
    1512             Main.debug("getTileForPixelpos("+px+", "+py+")");
    1513         }
    1514         MapView mv = Main.map.mapView;
    1515         Point clicked = new Point(px, py);
    1516         EastNorth topLeft = mv.getEastNorth(0, 0);
    1517         EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    1518         int z = currentZoomLevel;
    1519         TileSet ts = new TileSet(topLeft, botRight, z);
    1520 
    1521         if (!ts.tooLarge()) {
    1522             ts.loadAllTiles(false); // make sure there are tile objects for all tiles
    1523         }
    1524         Tile clickedTile = null;
    1525         for (Tile t1 : ts.allExistingTiles()) {
    1526             Tile t2 = tempCornerTile(t1);
    1527             Rectangle r = new Rectangle(pixelPos(t1));
    1528             r.add(pixelPos(t2));
    1529             if (Main.isDebugEnabled()) {
    1530                 Main.debug("r: " + r + " clicked: " + clicked);
    1531             }
    1532             if (!r.contains(clicked)) {
    1533                 continue;
    1534             }
    1535             clickedTile  = t1;
    1536             break;
    1537         }
    1538         if (clickedTile == null)
    1539             return null;
    1540         /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
    1541                 " currentZoomLevel: " + currentZoomLevel);*/
    1542         return clickedTile;
    1543     }
    1544 
    1545     @Override
    1546     public Action[] getMenuEntries() {
    1547         return new Action[] {
    1548                 LayerListDialog.getInstance().createShowHideLayerAction(),
    1549                 LayerListDialog.getInstance().createDeleteLayerAction(),
    1550                 SeparatorLayerAction.INSTANCE,
    1551                 // color,
    1552                 new OffsetAction(),
    1553                 new RenameLayerAction(this.getAssociatedFile(), this),
    1554                 SeparatorLayerAction.INSTANCE,
    1555                 new LayerListPopup.InfoAction(this) };
    1556     }
    1557 
    1558     @Override
    1559     public String getToolTipText() {
    1560         return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
    1561     }
    1562 
    1563     @Override
    1564     public void visitBoundingBox(BoundingXYVisitor v) {
    1565     }
    1566 
    1567     @Override
    1568     public boolean isChanged() {
    1569         return needRedraw;
    1570     }
    1571 
    1572     @Override
    1573     public final boolean isProjectionSupported(Projection proj) {
    1574         return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
    1575     }
    1576 
    1577     @Override
    1578     public final String nameSupportedProjections() {
    1579         return tr("EPSG:4326 and Mercator projection are supported");
    1580     }
    1581146}
  • trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java

    r8513 r8526  
    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;
     16import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     17import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     18import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    4019import 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;
     20import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
    5021import org.openstreetmap.josm.data.imagery.ImageryInfo;
    5122import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    5223import 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;
     24import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
     25import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     26import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
    5627import org.openstreetmap.josm.data.preferences.BooleanProperty;
    5728import org.openstreetmap.josm.data.preferences.IntegerProperty;
    5829import org.openstreetmap.josm.data.projection.Projection;
     30import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
    5931import org.openstreetmap.josm.gui.MapView;
    6032import 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;
    7133
    7234/**
    7335 * This is a layer that grabs the current screen from an WMS server. The data
    7436 * fetched this way is tiled and managed to the disc to reduce server load.
     37 *
    7538 */
    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);
     39public class WMSLayer extends AbstractTileSourceLayer {
     40    /** default tile size for WMS Layer */
     41    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 512);
     42    /** should WMS layer autozoom in default mode */
    11643    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true);
    117 
    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;
    16044
    16145    /**
    16246     * Constructs a new {@code WMSLayer}.
    163      */
    164     public WMSLayer() {
    165         this(new ImageryInfo(tr("Blank Layer")));
    166     }
    167 
    168     /**
    169      * Constructs a new {@code WMSLayer}.
     47     * @param info ImageryInfo description of the layer
    17048     */
    17149    public WMSLayer(ImageryInfo info) {
    17250        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);
    18051    }
    18152
    18253    @Override
    18354    public void hookUpMapView() {
    184         if (info.getUrl() != null) {
    185             startGrabberThreads();
     55        super.hookUpMapView();
     56        final ProjectionChangeListener listener = new ProjectionChangeListener() {
     57            @Override
     58            public void projectionChanged(Projection oldValue, Projection newValue) {
     59                if (!oldValue.equals(newValue) && tileSource instanceof TemplatedWMSTileSource) {
     60                    ((TemplatedWMSTileSource)tileSource).initProjection(newValue);
     61                }
    18662
    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                 }
    21163            }
    21264        };
    213         Main.map.mapView.addMouseListener(adapter);
     65        Main.addProjectionChangeListener(listener);
    21466
    21567        MapView.addLayerChangeListener(new LayerChangeListener() {
    21668            @Override
    21769            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
    218                 //
     70                // empty
    21971            }
    22072
    22173            @Override
    22274            public void layerAdded(Layer newLayer) {
    223                 //
     75                // empty
    22476            }
    22577
     
    22779            public void layerRemoved(Layer oldLayer) {
    22880                if (oldLayer == WMSLayer.this) {
    229                     Main.map.mapView.removeMouseListener(adapter);
    230                     MapView.removeLayerChangeListener(this);
     81                    Main.removeProjectionChangeListener(listener);
    23182                }
    23283            }
     
    23485    }
    23586
    236     public void doSetName(String name) {
    237         setName(name);
    238         info.setName(name);
     87    @Override
     88    public Action[] getMenuEntries() {
     89        List<Action> ret = new ArrayList<>();
     90        ret.addAll(Arrays.asList(super.getMenuEntries()));
     91        ret.add(SeparatorLayerAction.INSTANCE);
     92        ret.add(new LayerSaveAction(this));
     93        ret.add(new LayerSaveAsAction(this));
     94        ret.add(new BookmarkWmsAction());
     95        return ret.toArray(new Action[]{});
    23996    }
    24097
    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     }
    28198
    28299    @Override
    283     public void destroy() {
    284         super.destroy();
    285         cancelGrabberThreads(false);
    286         Main.pref.removePreferenceChangeListener(this);
    287         if (cache != null) {
    288             cache.saveIndex();
     100    protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
     101        if (info.getImageryType() == ImageryType.WMS && info.getUrl() != null) {
     102            TemplatedWMSTileSource.checkUrl(info.getUrl());
     103            TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info);
     104            info.setAttribution(tileSource);
     105            return tileSource;
    289106        }
    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;
    329     }
    330 
    331     @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);
    359         }
    360 
    361         attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
    362     }
    363 
    364     @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      * Returns east/north for a x/y couple.
    436      * @param xIndex x index
    437      * @param yIndex y index
    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
    490     public void visitBoundingBox(BoundingXYVisitor v) {
    491         for (int x = 0; x < dax; ++x) {
    492             for (int y = 0; y < day; ++y) {
    493                 if (images[x][y].getImage() != null) {
    494                     v.visit(images[x][y].getMin());
    495                     v.visit(images[x][y].getMax());
    496                 }
    497             }
    498         }
    499     }
    500 
    501     @Override
    502     public Action[] getMenuEntries() {
    503         return new Action[]{
    504                 LayerListDialog.getInstance().createActivateLayerAction(this),
    505                 LayerListDialog.getInstance().createShowHideLayerAction(),
    506                 LayerListDialog.getInstance().createDeleteLayerAction(),
    507                 SeparatorLayerAction.INSTANCE,
    508                 new OffsetAction(),
    509                 new LayerSaveAction(this),
    510                 new LayerSaveAsAction(this),
    511                 new BookmarkWmsAction(),
    512                 SeparatorLayerAction.INSTANCE,
    513                 new StartStopAction(),
    514                 new ToggleAlphaAction(),
    515                 new ToggleAutoResolutionAction(),
    516                 new ChangeResolutionAction(),
    517                 new ZoomToNativeResolution(),
    518                 new ReloadErrorTilesAction(),
    519                 new DownloadAction(),
    520                 SeparatorLayerAction.INSTANCE,
    521                 new LayerListPopup.InfoAction(this)
    522         };
    523     }
    524 
    525     public GeorefImage findImage(EastNorth eastNorth) {
    526         int xIndex = getImageXIndex(eastNorth.east());
    527         int yIndex = getImageYIndex(eastNorth.north());
    528         GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
    529         if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
    530             return result;
    531         else
    532             return null;
    533     }
    534 
    535     /**
    536      * Replies request priority.
    537      * @param request WMS request
    538      * @return -1 if request is no longer needed, otherwise priority of request (lower number &lt;=&gt; more important request)
    539      */
    540     private int getRequestPriority(WMSRequest request) {
    541         if (!Utils.equalsEpsilon(request.getPixelPerDegree(), info.getPixelPerDegree()))
    542             return -1;
    543         if (bminx > request.getXIndex()
    544                 || bmaxx < request.getXIndex()
    545                 || bminy > request.getYIndex()
    546                 || bmaxy < request.getYIndex())
    547             return -1;
    548 
    549         MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
    550         EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
    551         int mouseX = getImageXIndex(cursorEastNorth.east());
    552         int mouseY = getImageYIndex(cursorEastNorth.north());
    553         int dx = request.getXIndex() - mouseX;
    554         int dy = request.getYIndex() - mouseY;
    555 
    556         return 1 + dx * dx + dy * dy;
    557     }
    558 
    559     private void sortRequests(boolean localOnly) {
    560         Iterator<WMSRequest> it = requestQueue.iterator();
    561         while (it.hasNext()) {
    562             WMSRequest item = it.next();
    563 
    564             if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
    565                 it.remove();
    566                 continue;
    567             }
    568 
    569             int priority = getRequestPriority(item);
    570             if (priority == -1 && item.isPrecacheOnly()) {
    571                 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
    572             }
    573 
    574             if (localOnly && !item.hasExactMatch()) {
    575                 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
    576             }
    577 
    578             if (priority == -1
    579                     || finishedRequests.contains(item)
    580                     || processingRequests.contains(item)) {
    581                 it.remove();
    582             } else {
    583                 item.setPriority(priority);
    584             }
    585         }
    586         Collections.sort(requestQueue);
    587     }
    588 
    589     public WMSRequest getRequest(boolean localOnly) {
    590         requestQueueLock.lock();
    591         try {
    592             sortRequests(localOnly);
    593             while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
    594                 try {
    595                     queueEmpty.await();
    596                     sortRequests(localOnly);
    597                 } catch (InterruptedException e) {
    598                     Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
    599                 }
    600             }
    601 
    602             if (canceled)
    603                 return null;
    604             else {
    605                 WMSRequest request = requestQueue.remove(0);
    606                 processingRequests.add(request);
    607                 return request;
    608             }
    609 
    610         } finally {
    611             requestQueueLock.unlock();
    612         }
    613     }
    614 
    615     public void finishRequest(WMSRequest request) {
    616         requestQueueLock.lock();
    617         try {
    618             PrecacheTask task = request.getPrecacheTask();
    619             if (task != null) {
    620                 task.processedCount++;
    621                 if (!task.progressMonitor.isCanceled()) {
    622                     task.progressMonitor.worked(1);
    623                     task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
    624                 }
    625             }
    626             processingRequests.remove(request);
    627             if (request.getState() != null && !request.isPrecacheOnly()) {
    628                 finishedRequests.add(request);
    629                 if (Main.isDisplayingMapView()) {
    630                     Main.map.mapView.repaint();
    631                 }
    632             }
    633         } finally {
    634             requestQueueLock.unlock();
    635         }
    636     }
    637 
    638     public void addRequest(WMSRequest request) {
    639         requestQueueLock.lock();
    640         try {
    641 
    642             if (cache != null) {
    643                 ProjectionBounds b = getBounds(request);
    644                 // Checking for exact match is fast enough, no need to do it in separated thread
    645                 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
    646                 if (request.isPrecacheOnly() && request.hasExactMatch())
    647                     return; // We already have this tile cached
    648             }
    649 
    650             if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
    651                 requestQueue.add(request);
    652                 if (request.getPrecacheTask() != null) {
    653                     request.getPrecacheTask().totalCount++;
    654                 }
    655                 queueEmpty.signalAll();
    656             }
    657         } finally {
    658             requestQueueLock.unlock();
    659         }
    660     }
    661 
    662     public boolean requestIsVisible(WMSRequest request) {
    663         return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
    664     }
    665 
    666     private void gatherFinishedRequests() {
    667         requestQueueLock.lock();
    668         try {
    669             for (WMSRequest request: finishedRequests) {
    670                 GeorefImage img = images[modulo(request.getXIndex(), dax)][modulo(request.getYIndex(), day)];
    671                 if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
    672                     WMSException we = request.getException();
    673                     img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null);
    674                 }
    675             }
    676         } finally {
    677             requestQueueLock.unlock();
    678             finishedRequests.clear();
    679         }
    680     }
    681 
    682     public class DownloadAction extends AbstractAction {
    683         /**
    684          * Constructs a new {@code DownloadAction}.
    685          */
    686         public DownloadAction() {
    687             super(tr("Download visible tiles"));
    688         }
    689 
    690         @Override
    691         public void actionPerformed(ActionEvent ev) {
    692             if (zoomIsTooBig()) {
    693                 JOptionPane.showMessageDialog(
    694                         Main.parent,
    695                         tr("The requested area is too big. Please zoom in a little, or change resolution"),
    696                         tr("Error"),
    697                         JOptionPane.ERROR_MESSAGE
    698                         );
    699             } else {
    700                 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
    701             }
    702         }
    703     }
    704 
    705     /**
    706      * Finds the most suitable resolution for the current zoom level, but prefers
    707      * higher resolutions. Snaps to values defined in snapLevels.
    708      * @return best zoom level
    709      */
    710     private static double getBestZoom() {
    711         // not sure why getDist100Pixel returns values corresponding to
    712         // the snapLevels, which are in meters per pixel. It works, though.
    713         double dist = Main.map.mapView.getDist100Pixel();
    714         for (int i = snapLevels.length-2; i >= 0; i--) {
    715             if (snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
    716                 return snapLevels[i+1];
    717         }
    718         return snapLevels[0];
    719     }
    720 
    721     /**
    722      * Updates the given layer’s resolution settings to the current zoom level. Does
    723      * not update existing tiles, only new ones will be subject to the new settings.
    724      *
    725      * @param layer WMS layer
    726      * @param snap  Set to true if the resolution should snap to certain values instead of
    727      *              matching the current zoom level perfectly
    728      */
    729     private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
    730         if (snap) {
    731             layer.resolution = getBestZoom();
    732             layer.resolutionText = MapView.getDistText(layer.resolution);
    733         } else {
    734             layer.resolution = Main.map.mapView.getDist100Pixel();
    735             layer.resolutionText = Main.map.mapView.getDist100PixelText();
    736         }
    737         layer.info.setPixelPerDegree(layer.getPPD());
    738     }
    739 
    740     /**
    741      * Updates the given layer’s resolution settings to the current zoom level and
    742      * updates existing tiles. If round is true, tiles will be updated gradually, if
    743      * false they will be removed instantly (and redrawn only after the new resolution
    744      * image has been loaded).
    745      * @param layer WMS layer
    746      * @param snap  Set to true if the resolution should snap to certain values instead of
    747      *              matching the current zoom level perfectly
    748      */
    749     private static void changeResolution(WMSLayer layer, boolean snap) {
    750         updateResolutionSetting(layer, snap);
    751 
    752         layer.settingsChanged = true;
    753 
    754         // Don’t move tiles off screen when the resolution is rounded. This
    755         // prevents some flickering when zooming with auto-resolution enabled
    756         // and instead gradually updates each tile.
    757         if (!snap) {
    758             for (int x = 0; x < layer.dax; ++x) {
    759                 for (int y = 0; y < layer.day; ++y) {
    760                     layer.images[x][y].changePosition(-1, -1);
    761                 }
    762             }
    763         }
    764     }
    765 
    766     public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
    767 
    768         /**
    769          * Constructs a new {@code ChangeResolutionAction}
    770          */
    771         public ChangeResolutionAction() {
    772             super(tr("Change resolution"));
    773         }
    774 
    775         @Override
    776         public void actionPerformed(ActionEvent ev) {
    777             List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
    778             for (Layer l: layers) {
    779                 changeResolution((WMSLayer) l, false);
    780             }
    781             Main.map.mapView.repaint();
    782         }
    783 
    784         @Override
    785         public boolean supportLayers(List<Layer> layers) {
    786             for (Layer l: layers) {
    787                 if (!(l instanceof WMSLayer))
    788                     return false;
    789             }
    790             return true;
    791         }
    792 
    793         @Override
    794         public Component createMenuComponent() {
    795             return new JMenuItem(this);
    796         }
    797     }
    798 
    799     public class ReloadErrorTilesAction extends AbstractAction {
    800         /**
    801          * Constructs a new {@code ReloadErrorTilesAction}.
    802          */
    803         public ReloadErrorTilesAction() {
    804             super(tr("Reload erroneous tiles"));
    805         }
    806 
    807         @Override
    808         public void actionPerformed(ActionEvent ev) {
    809             // Delete small files, because they're probably blank tiles.
    810             // See #2307
    811             cache.cleanSmallFiles(4096);
    812 
    813             for (int x = 0; x < dax; ++x) {
    814                 for (int y = 0; y < day; ++y) {
    815                     GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
    816                     if (img.getState() == State.FAILED) {
    817                         addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
    818                     }
    819                 }
    820             }
    821         }
    822     }
    823 
    824     public class ToggleAlphaAction extends AbstractAction implements LayerAction {
    825         /**
    826          * Constructs a new {@code ToggleAlphaAction}.
    827          */
    828         public ToggleAlphaAction() {
    829             super(tr("Alpha channel"));
    830         }
    831 
    832         @Override
    833         public void actionPerformed(ActionEvent ev) {
    834             JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
    835             boolean alphaChannel = checkbox.isSelected();
    836             PROP_ALPHA_CHANNEL.put(alphaChannel);
    837             Main.info("WMS Alpha channel changed to "+alphaChannel);
    838 
    839             // clear all resized cached instances and repaint the layer
    840             for (int x = 0; x < dax; ++x) {
    841                 for (int y = 0; y < day; ++y) {
    842                     GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
    843                     img.flushResizedCachedInstance();
    844                     BufferedImage bi = img.getImage();
    845                     // Completely erases images for which transparency has been forced,
    846                     // or images that should be forced now, as they need to be recreated
    847                     if (ImageProvider.isTransparencyForced(bi) || ImageProvider.hasTransparentColor(bi)) {
    848                         img.resetImage();
    849                     }
    850                 }
    851             }
    852             Main.map.mapView.repaint();
    853         }
    854 
    855         @Override
    856         public Component createMenuComponent() {
    857             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    858             item.setSelected(PROP_ALPHA_CHANNEL.get());
    859             return item;
    860         }
    861 
    862         @Override
    863         public boolean supportLayers(List<Layer> layers) {
    864             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    865         }
    866     }
    867 
    868     public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
    869 
    870         /**
    871          * Constructs a new {@code ToggleAutoResolutionAction}.
    872          */
    873         public ToggleAutoResolutionAction() {
    874             super(tr("Automatically change resolution"));
    875         }
    876 
    877         @Override
    878         public void actionPerformed(ActionEvent ev) {
    879             JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
    880             autoResolutionEnabled = checkbox.isSelected();
    881         }
    882 
    883         @Override
    884         public Component createMenuComponent() {
    885             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    886             item.setSelected(autoResolutionEnabled);
    887             return item;
    888         }
    889 
    890         @Override
    891         public boolean supportLayers(List<Layer> layers) {
    892             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    893         }
     107        return null;
    894108    }
    895109
     
    913127    }
    914128
    915     private class StartStopAction extends AbstractAction implements LayerAction {
    916 
    917         public StartStopAction() {
    918             super(tr("Automatic downloading"));
    919         }
    920 
    921         @Override
    922         public Component createMenuComponent() {
    923             JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
    924             item.setSelected(autoDownloadEnabled);
    925             return item;
    926         }
    927 
    928         @Override
    929         public boolean supportLayers(List<Layer> layers) {
    930             return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
    931         }
    932 
    933         @Override
    934         public void actionPerformed(ActionEvent e) {
    935             autoDownloadEnabled = !autoDownloadEnabled;
    936             if (autoDownloadEnabled) {
    937                 for (int x = 0; x < dax; ++x) {
    938                     for (int y = 0; y < day; ++y) {
    939                         GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
    940                         if (img.getState() == State.NOT_IN_CACHE) {
    941                             addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
    942                         }
    943                     }
    944                 }
    945                 Main.map.mapView.repaint();
    946             }
    947         }
    948     }
    949 
    950     private class ZoomToNativeResolution extends AbstractAction {
    951 
    952         public ZoomToNativeResolution() {
    953             super(tr("Zoom to native resolution"));
    954         }
    955 
    956         @Override
    957         public void actionPerformed(ActionEvent e) {
    958             Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
    959         }
    960     }
    961 
    962     private void cancelGrabberThreads(boolean wait) {
    963         requestQueueLock.lock();
    964         try {
    965             canceled = true;
    966             for (WMSGrabber grabber: grabbers) {
    967                 grabber.cancel();
    968             }
    969             queueEmpty.signalAll();
    970         } finally {
    971             requestQueueLock.unlock();
    972         }
    973         if (wait) {
    974             for (Thread t: grabberThreads) {
    975                 try {
    976                     t.join();
    977                 } catch (InterruptedException e) {
    978                     Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");
    979                 }
    980             }
    981         }
    982     }
    983 
    984     private void startGrabberThreads() {
    985         int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
    986         requestQueueLock.lock();
    987         try {
    988             canceled = false;
    989             grabbers.clear();
    990             grabberThreads.clear();
    991             for (int i = 0; i < threadCount; i++) {
    992                 WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);
    993                 grabbers.add(grabber);
    994                 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
    995                 t.setDaemon(true);
    996                 t.start();
    997                 grabberThreads.add(t);
    998             }
    999         } finally {
    1000             requestQueueLock.unlock();
    1001         }
    1002     }
    1003 
    1004     @Override
    1005     public boolean isChanged() {
    1006         requestQueueLock.lock();
    1007         try {
    1008             return !finishedRequests.isEmpty() || settingsChanged;
    1009         } finally {
    1010             requestQueueLock.unlock();
    1011         }
    1012     }
    1013 
    1014     @Override
    1015     public void preferenceChanged(PreferenceChangeEvent event) {
    1016         if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {
    1017             cancelGrabberThreads(true);
    1018             startGrabberThreads();
    1019         } else if (
    1020                 event.getKey().equals(PROP_OVERLAP.getKey())
    1021                 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
    1022                 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
    1023             for (int i = 0; i < images.length; i++) {
    1024                 for (int k = 0; k < images[i].length; k++) {
    1025                     images[i][k] = new GeorefImage(this);
    1026                 }
    1027             }
    1028 
    1029             settingsChanged = true;
    1030         }
    1031     }
    1032129
    1033130    /**
     
    1037134     */
    1038135    public void checkGrabberType() {
    1039         ImageryType it = getInfo().getImageryType();
    1040         if (!ImageryType.HTML.equals(it) && !ImageryType.WMS.equals(it))
    1041             throw new IllegalStateException("getGrabber() called for non-WMS layer type");
    1042136    }
    1043137
    1044     protected WMSGrabber getGrabber(boolean localOnly) {
    1045         checkGrabberType();
    1046         if (getInfo().getImageryType() == ImageryType.HTML)
    1047             return new HTMLGrabber(Main.map.mapView, this, localOnly);
    1048         else
    1049             return new WMSGrabber(Main.map.mapView, this, localOnly);
    1050     }
     138    private static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("WMS") {
     139        @Override
     140        protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout,
     141                int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
     142            return new WMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir);
     143        }
    1051144
    1052     public ProjectionBounds getBounds(WMSRequest request) {
    1053         ProjectionBounds result = new ProjectionBounds(
    1054                 getEastNorth(request.getXIndex(), request.getYIndex()),
    1055                 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
     145    };
    1056146
    1057         if (WMSLayer.PROP_OVERLAP.get()) {
    1058             double eastSize =  result.maxEast - result.minEast;
    1059             double northSize =  result.maxNorth - result.minNorth;
    1060 
    1061             double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
    1062             double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
    1063 
    1064             result = new ProjectionBounds(result.getMin(),
    1065                     new EastNorth(result.maxEast + eastCoef * eastSize,
    1066                             result.maxNorth + northCoef * northSize));
    1067         }
    1068         return result;
     147    @Override
     148    protected TileLoaderFactory getTileLoaderFactory() {
     149        return loaderFactory;
    1069150    }
    1070151
    1071152    @Override
    1072     public boolean isProjectionSupported(Projection proj) {
    1073         List<String> serverProjections = info.getServerProjections();
    1074         return serverProjections.contains(proj.toCode().toUpperCase(Locale.ENGLISH))
    1075                 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
    1076                 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
    1077     }
    1078 
    1079     @Override
    1080     public String nameSupportedProjections() {
    1081         StringBuilder res = new StringBuilder();
    1082         for (String p : info.getServerProjections()) {
    1083             if (res.length() > 0) {
    1084                 res.append(", ");
    1085             }
    1086             res.append(p);
     153    protected Map<String, String> getHeaders(TileSource tileSource) {
     154        if (tileSource instanceof TemplatedWMSTileSource) {
     155            return ((TemplatedWMSTileSource)tileSource).getHeaders();
    1087156        }
    1088         return tr("Supported projections are: {0}", res);
    1089     }
    1090 
    1091     @Override
    1092     public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    1093         boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
    1094         Main.map.repaint(done ? 0 : 100);
    1095         return !done;
    1096     }
    1097 
    1098     @Override
    1099     public void writeExternal(ObjectOutput out) throws IOException {
    1100         out.writeInt(serializeFormatVersion);
    1101         out.writeInt(dax);
    1102         out.writeInt(day);
    1103         out.writeInt(imageSize);
    1104         out.writeDouble(info.getPixelPerDegree());
    1105         out.writeObject(info.getName());
    1106         out.writeObject(info.getExtendedUrl());
    1107         out.writeObject(images);
    1108     }
    1109 
    1110     @Override
    1111     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    1112         int sfv = in.readInt();
    1113         if (sfv != serializeFormatVersion)
    1114             throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
    1115         autoDownloadEnabled = false;
    1116         dax = in.readInt();
    1117         day = in.readInt();
    1118         imageSize = in.readInt();
    1119         info.setPixelPerDegree(in.readDouble());
    1120         doSetName((String) in.readObject());
    1121         info.setExtendedUrl((String) in.readObject());
    1122         images = (GeorefImage[][]) in.readObject();
    1123 
    1124         for (GeorefImage[] imgs : images) {
    1125             for (GeorefImage img : imgs) {
    1126                 if (img != null) {
    1127                     img.setLayer(WMSLayer.this);
    1128                 }
    1129             }
    1130         }
    1131 
    1132         settingsChanged = true;
    1133         if (Main.isDisplayingMapView()) {
    1134             Main.map.mapView.repaint();
    1135         }
    1136         if (cache != null) {
    1137             cache.saveIndex();
    1138             cache = null;
    1139         }
    1140     }
    1141 
    1142     @Override
    1143     public void onPostLoadFromFile() {
    1144         if (info.getUrl() != null) {
    1145             cache = new WmsCache(info.getUrl(), imageSize);
    1146             startGrabberThreads();
    1147         }
    1148     }
    1149 
    1150     @Override
    1151     public boolean isSavable() {
    1152         return true; // With WMSLayerExporter
    1153     }
    1154 
    1155     @Override
    1156     public File createAndOpenSaveFileChooser() {
    1157         return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
     157        return null;
    1158158    }
    1159159}
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java

    r8308 r8526  
    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;
     
    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"));
     
    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")) {
     
    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()) {
     
    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);
     
    114121            return null;
    115122        }
    116         return (WMSLayer) layerList.getSelectedItem();
     123        return (AbstractTileSourceLayer) layerList.getSelectedItem();
    117124    }
    118125
  • trunk/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java

    r8510 r8526  
    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;
     
    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));
     
    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());
     
    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
  • trunk/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java

    r8426 r8526  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.FlowLayout;
    76import java.awt.GridBagLayout;
    87
     
    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
     
    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    /**
     
    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));
    6246        labelSimConn.setLabelFor(spinSimConn);
    6347        add(labelSimConn, GBC.std());
     
    6549        add(spinSimConn, GBC.eol());
    6650
    67         // Overlap
    68         add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL));
    69 
    70         overlapCheckBox = new JCheckBox(tr("Overlap tiles"));
    71         JLabel labelEast = new JLabel(tr("% of east:"));
    72         JLabel labelNorth = new JLabel(tr("% of north:"));
    73         spinEast = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_EAST.get().intValue(), 1, 50, 1));
    74         spinNorth = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_NORTH.get().intValue(), 1, 50, 1));
    75         labelEast.setLabelFor(spinEast);
    76         labelNorth.setLabelFor(spinNorth);
    77 
    78         JPanel overlapPanel = new JPanel(new FlowLayout());
    79         overlapPanel.add(overlapCheckBox);
    80         overlapPanel.add(labelEast);
    81         overlapPanel.add(spinEast);
    82         overlapPanel.add(labelNorth);
    83         overlapPanel.add(spinNorth);
    84 
    85         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());
    8658    }
    8759
     
    9163    public void loadSettings() {
    9264        this.autozoomActive.setSelected(WMSLayer.PROP_DEFAULT_AUTOZOOM.get());
    93         this.browser.setSelectedItem(HTMLGrabber.PROP_BROWSER.get());
    94         this.overlapCheckBox.setSelected(WMSLayer.PROP_OVERLAP.get());
    95         this.spinEast.setValue(WMSLayer.PROP_OVERLAP_EAST.get());
    96         this.spinNorth.setValue(WMSLayer.PROP_OVERLAP_NORTH.get());
    97         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());
    9867    }
    9968
     
    10473    public boolean saveSettings() {
    10574        WMSLayer.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected());
    106         WMSLayer.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
    107         WMSLayer.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
    108         WMSLayer.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
    109         WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
    110 
    111         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());
    11277
    11378        return false;
  • trunk/src/org/openstreetmap/josm/io/WMSLayerExporter.java

    r8510 r8526  
    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
     
    1720 */
    1821public class WMSLayerExporter extends FileExporter {
     22
     23    /** Which version of the file we export */
     24    public static final int CURRENT_FILE_VERSION = 6;
    1925
    2026    /**
     
    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}
  • trunk/src/org/openstreetmap/josm/io/WMSLayerImporter.java

    r7033 r8526  
    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;
     
    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());
     40        super(FILE_FILTER);
    3741    }
    3842
    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) {
    44         super(FILE_FILTER);
    45         this.wmsLayer = wmsLayer;
    46     }
    4743
    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
     
    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);
     94                }
    6395            }
    6496        });
    6597    }
    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;
    74     }
    7598}
  • trunk/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java

    r8510 r8526  
    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;
     
    8182        ImageryPreferenceEntry e = new ImageryPreferenceEntry(layer.getInfo());
    8283        Map<String, String> data = new LinkedHashMap<>(Preferences.serializeStruct(e, ImageryPreferenceEntry.class));
    83         if (layer instanceof WMSLayer) {
    84             WMSLayer wms = (WMSLayer) layer;
    85             data.put("automatic-downloading", Boolean.toString(wms.hasAutoDownload()));
    86             data.put("automatically-change-resolution", Boolean.toString(wms.isAutoResolution()));
     84        if (layer instanceof AbstractTileSourceLayer) {
     85            AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer;
     86            data.put("automatic-downloading", Boolean.toString(tsLayer.autoLoad));
     87            data.put("automatically-change-resolution", Boolean.toString(tsLayer.autoZoom));
     88            data.put("show-errors", Boolean.toString(tsLayer.showErrors));
    8789        }
    8890        for (Map.Entry<String, String> entry : data.entrySet()) {
  • trunk/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java

    r8510 r8526  
    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;
     
    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));
     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();
    5352            }
    54             String autoResolution = attributes.get("automatically-change-resolution");
    55             if (autoResolution != null) {
    56                 wms.setAutoResolution(Boolean.parseBoolean(autoResolution));
     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        }
Note: See TracChangeset for help on using the changeset viewer.