Ignore:
Timestamp:
2015-06-24T20:57:43+02:00 (9 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
File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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}
Note: See TracChangeset for help on using the changeset viewer.