001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Graphics;
008import java.awt.Graphics2D;
009import java.awt.Image;
010import java.awt.Point;
011import java.awt.event.ActionEvent;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.awt.image.BufferedImage;
015import java.awt.image.ImageObserver;
016import java.io.Externalizable;
017import java.io.File;
018import java.io.IOException;
019import java.io.InvalidClassException;
020import java.io.ObjectInput;
021import java.io.ObjectOutput;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Set;
028import java.util.concurrent.locks.Condition;
029import java.util.concurrent.locks.Lock;
030import java.util.concurrent.locks.ReentrantLock;
031
032import javax.swing.AbstractAction;
033import javax.swing.Action;
034import javax.swing.JCheckBoxMenuItem;
035import javax.swing.JMenuItem;
036import javax.swing.JOptionPane;
037
038import org.openstreetmap.gui.jmapviewer.AttributionSupport;
039import org.openstreetmap.josm.Main;
040import org.openstreetmap.josm.actions.SaveActionBase;
041import org.openstreetmap.josm.data.Bounds;
042import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
043import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
044import org.openstreetmap.josm.data.ProjectionBounds;
045import org.openstreetmap.josm.data.coor.EastNorth;
046import org.openstreetmap.josm.data.coor.LatLon;
047import org.openstreetmap.josm.data.imagery.GeorefImage;
048import org.openstreetmap.josm.data.imagery.GeorefImage.State;
049import org.openstreetmap.josm.data.imagery.ImageryInfo;
050import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
051import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
052import org.openstreetmap.josm.data.imagery.WmsCache;
053import org.openstreetmap.josm.data.imagery.types.ObjectFactory;
054import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
055import org.openstreetmap.josm.data.preferences.BooleanProperty;
056import org.openstreetmap.josm.data.preferences.IntegerProperty;
057import org.openstreetmap.josm.data.projection.Projection;
058import org.openstreetmap.josm.gui.MapView;
059import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
060import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
061import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
062import org.openstreetmap.josm.gui.progress.ProgressMonitor;
063import org.openstreetmap.josm.io.WMSLayerImporter;
064import org.openstreetmap.josm.io.imagery.Grabber;
065import org.openstreetmap.josm.io.imagery.HTMLGrabber;
066import org.openstreetmap.josm.io.imagery.WMSGrabber;
067import org.openstreetmap.josm.io.imagery.WMSRequest;
068
069
070/**
071 * This is a layer that grabs the current screen from an WMS server. The data
072 * fetched this way is tiled and managed to the disc to reduce server load.
073 */
074public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener, Externalizable {
075
076    public static class PrecacheTask {
077        private final ProgressMonitor progressMonitor;
078        private volatile int totalCount;
079        private volatile int processedCount;
080        private volatile boolean isCancelled;
081
082        public PrecacheTask(ProgressMonitor progressMonitor) {
083            this.progressMonitor = progressMonitor;
084        }
085
086        public boolean isFinished() {
087            return totalCount == processedCount;
088        }
089
090        public int getTotalCount() {
091            return totalCount;
092        }
093
094        public void cancel() {
095            isCancelled = true;
096        }
097    }
098
099    // Fake reference to keep build scripts from removing ObjectFactory class. This class is not used directly but it's necessary for jaxb to work
100    private static final ObjectFactory OBJECT_FACTORY = null;
101
102    // these values correspond to the zoom levels used throughout OSM and are in meters/pixel from zoom level 0 to 18.
103    // taken from http://wiki.openstreetmap.org/wiki/Zoom_levels
104    private static final Double[] snapLevels = { 156412.0, 78206.0, 39103.0, 19551.0, 9776.0, 4888.0,
105        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 };
106
107    public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true);
108    public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3);
109    public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false);
110    public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14);
111    public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4);
112    public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 500);
113    public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true);
114
115    public int messageNum = 5; //limit for messages per layer
116    protected double resolution;
117    protected String resolutionText;
118    protected int imageSize;
119    protected int dax = 10;
120    protected int day = 10;
121    protected int daStep = 5;
122    protected int minZoom = 3;
123
124    protected GeorefImage[][] images;
125    protected static final int serializeFormatVersion = 5;
126    protected boolean autoDownloadEnabled = true;
127    protected boolean autoResolutionEnabled = PROP_DEFAULT_AUTOZOOM.get();
128    protected boolean settingsChanged;
129    public WmsCache cache;
130    private AttributionSupport attribution = new AttributionSupport();
131
132    // Image index boundary for current view
133    private volatile int bminx;
134    private volatile int bminy;
135    private volatile int bmaxx;
136    private volatile int bmaxy;
137    private volatile int leftEdge;
138    private volatile int bottomEdge;
139
140    // Request queue
141    private final List<WMSRequest> requestQueue = new ArrayList<WMSRequest>();
142    private final List<WMSRequest> finishedRequests = new ArrayList<WMSRequest>();
143    /**
144     * List of request currently being processed by download threads
145     */
146    private final List<WMSRequest> processingRequests = new ArrayList<WMSRequest>();
147    private final Lock requestQueueLock = new ReentrantLock();
148    private final Condition queueEmpty = requestQueueLock.newCondition();
149    private final List<Grabber> grabbers = new ArrayList<Grabber>();
150    private final List<Thread> grabberThreads = new ArrayList<Thread>();
151    private boolean canceled;
152
153    /** set to true if this layer uses an invalid base url */
154    private boolean usesInvalidUrl = false;
155    /** set to true if the user confirmed to use an potentially invalid WMS base url */
156    private boolean isInvalidUrlConfirmed = false;
157
158    public WMSLayer() {
159        this(new ImageryInfo(tr("Blank Layer")));
160    }
161
162    public WMSLayer(ImageryInfo info) {
163        super(info);
164        imageSize = PROP_IMAGE_SIZE.get();
165        setBackgroundLayer(true); /* set global background variable */
166        initializeImages();
167
168        attribution.initialize(this.info);
169
170        Main.pref.addPreferenceChangeListener(this);
171    }
172
173    @Override
174    public void hookUpMapView() {
175        if (info.getUrl() != null) {
176            startGrabberThreads();
177
178            for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {
179                if (layer.getInfo().getUrl().equals(info.getUrl())) {
180                    cache = layer.cache;
181                    break;
182                }
183            }
184            if (cache == null) {
185                cache = new WmsCache(info.getUrl(), imageSize);
186                cache.loadIndex();
187            }
188        }
189
190        // if automatic resolution is enabled, ensure that the first zoom level
191        // is already snapped. Otherwise it may load tiles that will never get
192        // used again when zooming.
193        updateResolutionSetting(this, autoResolutionEnabled);
194
195        final MouseAdapter adapter = new MouseAdapter() {
196            @Override
197            public void mouseClicked(MouseEvent e) {
198                if (!isVisible()) return;
199                if (e.getButton() == MouseEvent.BUTTON1) {
200                    attribution.handleAttribution(e.getPoint(), true);
201                }
202            }
203        };
204        Main.map.mapView.addMouseListener(adapter);
205
206        MapView.addLayerChangeListener(new LayerChangeListener() {
207            @Override
208            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
209                //
210            }
211
212            @Override
213            public void layerAdded(Layer newLayer) {
214                //
215            }
216
217            @Override
218            public void layerRemoved(Layer oldLayer) {
219                if (oldLayer == WMSLayer.this) {
220                    Main.map.mapView.removeMouseListener(adapter);
221                    MapView.removeLayerChangeListener(this);
222                }
223            }
224        });
225    }
226
227    public void doSetName(String name) {
228        setName(name);
229        info.setName(name);
230    }
231
232    public boolean hasAutoDownload(){
233        return autoDownloadEnabled;
234    }
235
236    public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
237        Set<Point> requestedTiles = new HashSet<Point>();
238        for (LatLon point: points) {
239            EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));
240            EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));
241            int minX = getImageXIndex(minEn.east());
242            int maxX = getImageXIndex(maxEn.east());
243            int minY = getImageYIndex(minEn.north());
244            int maxY = getImageYIndex(maxEn.north());
245
246            for (int x=minX; x<=maxX; x++) {
247                for (int y=minY; y<=maxY; y++) {
248                    requestedTiles.add(new Point(x, y));
249                }
250            }
251        }
252
253        for (Point p: requestedTiles) {
254            addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));
255        }
256
257        precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());
258        precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));
259    }
260
261    @Override
262    public void destroy() {
263        super.destroy();
264        cancelGrabberThreads(false);
265        Main.pref.removePreferenceChangeListener(this);
266        if (cache != null) {
267            cache.saveIndex();
268        }
269    }
270
271    public void initializeImages() {
272        GeorefImage[][] old = images;
273        images = new GeorefImage[dax][day];
274        if (old != null) {
275            for (GeorefImage[] row : old) {
276                for (GeorefImage image : row) {
277                    images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;
278                }
279            }
280        }
281        for(int x = 0; x<dax; ++x) {
282            for(int y = 0; y<day; ++y) {
283                if (images[x][y] == null) {
284                    images[x][y]= new GeorefImage(this);
285                }
286            }
287        }
288    }
289
290    @Override public ImageryInfo getInfo() {
291        return info;
292    }
293
294    @Override public String getToolTipText() {
295        if(autoDownloadEnabled)
296            return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);
297        else
298            return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);
299    }
300
301    private int modulo (int a, int b) {
302        return a % b >= 0 ? a%b : a%b+b;
303    }
304
305    private boolean zoomIsTooBig() {
306        //don't download when it's too outzoomed
307        return info.getPixelPerDegree() / getPPD() > minZoom;
308    }
309
310    @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
311        if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
312
313        if (autoResolutionEnabled && getBestZoom() != mv.getDist100Pixel()) {
314            changeResolution(this, true);
315        }
316
317        settingsChanged = false;
318
319        ProjectionBounds bounds = mv.getProjectionBounds();
320        bminx= getImageXIndex(bounds.minEast);
321        bminy= getImageYIndex(bounds.minNorth);
322        bmaxx= getImageXIndex(bounds.maxEast);
323        bmaxy= getImageYIndex(bounds.maxNorth);
324
325        leftEdge = (int)(bounds.minEast * getPPD());
326        bottomEdge = (int)(bounds.minNorth * getPPD());
327
328        if (zoomIsTooBig()) {
329            for(int x = 0; x<images.length; ++x) {
330                for(int y = 0; y<images[0].length; ++y) {
331                    GeorefImage image = images[x][y];
332                    image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
333                }
334            }
335        } else {
336            downloadAndPaintVisible(g, mv, false);
337        }
338
339        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
340
341    }
342
343    @Override
344    public void setOffset(double dx, double dy) {
345        super.setOffset(dx, dy);
346        settingsChanged = true;
347    }
348
349    public int getImageXIndex(double coord) {
350        return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
351    }
352
353    public int getImageYIndex(double coord) {
354        return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
355    }
356
357    public int getImageX(int imageIndex) {
358        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
359    }
360
361    public int getImageY(int imageIndex) {
362        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
363    }
364
365    public int getImageWidth(int xIndex) {
366        return getImageX(xIndex + 1) - getImageX(xIndex);
367    }
368
369    public int getImageHeight(int yIndex) {
370        return getImageY(yIndex + 1) - getImageY(yIndex);
371    }
372
373    /**
374     *
375     * @return Size of image in original zoom
376     */
377    public int getBaseImageWidth() {
378        int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_EAST.get() * imageSize / 100) : 0;
379        return imageSize + overlap;
380    }
381
382    /**
383     *
384     * @return Size of image in original zoom
385     */
386    public int getBaseImageHeight() {
387        int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_NORTH.get() * imageSize / 100) : 0;
388        return imageSize + overlap;
389    }
390
391    public int getImageSize() {
392        return imageSize;
393    }
394
395    public boolean isOverlapEnabled() {
396        return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0);
397    }
398
399    /**
400     *
401     * @return When overlapping is enabled, return visible part of tile. Otherwise return original image
402     */
403    public BufferedImage normalizeImage(BufferedImage img) {
404        if (isOverlapEnabled()) {
405            BufferedImage copy = img;
406            img = new BufferedImage(imageSize, imageSize, copy.getType());
407            img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize,
408                    0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null);
409        }
410        return img;
411    }
412
413    /**
414     *
415     * @param xIndex
416     * @param yIndex
417     * @return Real EastNorth of given tile. dx/dy is not counted in
418     */
419    public EastNorth getEastNorth(int xIndex, int yIndex) {
420        return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
421    }
422
423    protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
424
425        int newDax = dax;
426        int newDay = day;
427
428        if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
429            newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
430        }
431
432        if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
433            newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
434        }
435
436        if (newDax != dax || newDay != day) {
437            dax = newDax;
438            day = newDay;
439            initializeImages();
440        }
441
442        for(int x = bminx; x<=bmaxx; ++x) {
443            for(int y = bminy; y<=bmaxy; ++y){
444                images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
445            }
446        }
447
448        gatherFinishedRequests();
449        Set<ProjectionBounds> areaToCache = new HashSet<ProjectionBounds>();
450
451        for(int x = bminx; x<=bmaxx; ++x) {
452            for(int y = bminy; y<=bmaxy; ++y){
453                GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
454                if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
455                    WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true);
456                    addRequest(request);
457                    areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
458                } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
459                    WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false);
460                    addRequest(request);
461                    areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
462                }
463            }
464        }
465        if (cache != null) {
466            cache.setAreaToCache(areaToCache);
467        }
468    }
469
470    @Override public void visitBoundingBox(BoundingXYVisitor v) {
471        for(int x = 0; x<dax; ++x) {
472            for(int y = 0; y<day; ++y)
473                if(images[x][y].getImage() != null){
474                    v.visit(images[x][y].getMin());
475                    v.visit(images[x][y].getMax());
476                }
477        }
478    }
479
480    @Override public Action[] getMenuEntries() {
481        return new Action[]{
482                LayerListDialog.getInstance().createActivateLayerAction(this),
483                LayerListDialog.getInstance().createShowHideLayerAction(),
484                LayerListDialog.getInstance().createDeleteLayerAction(),
485                SeparatorLayerAction.INSTANCE,
486                new OffsetAction(),
487                new LayerSaveAction(this),
488                new LayerSaveAsAction(this),
489                new BookmarkWmsAction(),
490                SeparatorLayerAction.INSTANCE,
491                new StartStopAction(),
492                new ToggleAlphaAction(),
493                new ToggleAutoResolutionAction(),
494                new ChangeResolutionAction(),
495                new ZoomToNativeResolution(),
496                new ReloadErrorTilesAction(),
497                new DownloadAction(),
498                SeparatorLayerAction.INSTANCE,
499                new LayerListPopup.InfoAction(this)
500        };
501    }
502
503    public GeorefImage findImage(EastNorth eastNorth) {
504        int xIndex = getImageXIndex(eastNorth.east());
505        int yIndex = getImageYIndex(eastNorth.north());
506        GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
507        if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
508            return result;
509        else
510            return null;
511    }
512
513    /**
514     *
515     * @param request
516     * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)
517     */
518    private int getRequestPriority(WMSRequest request) {
519        if (request.getPixelPerDegree() != info.getPixelPerDegree())
520            return -1;
521        if (bminx > request.getXIndex()
522                || bmaxx < request.getXIndex()
523                || bminy > request.getYIndex()
524                || bmaxy < request.getYIndex())
525            return -1;
526
527        MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
528        EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
529        int mouseX = getImageXIndex(cursorEastNorth.east());
530        int mouseY = getImageYIndex(cursorEastNorth.north());
531        int dx = request.getXIndex() - mouseX;
532        int dy = request.getYIndex() - mouseY;
533
534        return 1 + dx * dx + dy * dy;
535    }
536
537    private void sortRequests(boolean localOnly) {
538        Iterator<WMSRequest> it = requestQueue.iterator();
539        while (it.hasNext()) {
540            WMSRequest item = it.next();
541
542            if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
543                it.remove();
544                continue;
545            }
546
547            int priority = getRequestPriority(item);
548            if (priority == -1 && item.isPrecacheOnly()) {
549                priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
550            }
551
552            if (localOnly && !item.hasExactMatch()) {
553                priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
554            }
555
556            if (       priority == -1
557                    || finishedRequests.contains(item)
558                    || processingRequests.contains(item)) {
559                it.remove();
560            } else {
561                item.setPriority(priority);
562            }
563        }
564        Collections.sort(requestQueue);
565    }
566
567    public WMSRequest getRequest(boolean localOnly) {
568        requestQueueLock.lock();
569        try {
570            sortRequests(localOnly);
571            while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
572                try {
573                    queueEmpty.await();
574                    sortRequests(localOnly);
575                } catch (InterruptedException e) {
576                    Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request");
577                }
578            }
579
580            if (canceled)
581                return null;
582            else {
583                WMSRequest request = requestQueue.remove(0);
584                processingRequests.add(request);
585                return request;
586            }
587
588        } finally {
589            requestQueueLock.unlock();
590        }
591    }
592
593    public void finishRequest(WMSRequest request) {
594        requestQueueLock.lock();
595        try {
596            PrecacheTask task = request.getPrecacheTask();
597            if (task != null) {
598                task.processedCount++;
599                if (!task.progressMonitor.isCanceled()) {
600                    task.progressMonitor.worked(1);
601                    task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
602                }
603            }
604            processingRequests.remove(request);
605            if (request.getState() != null && !request.isPrecacheOnly()) {
606                finishedRequests.add(request);
607                if (Main.isDisplayingMapView()) {
608                    Main.map.mapView.repaint();
609                }
610            }
611        } finally {
612            requestQueueLock.unlock();
613        }
614    }
615
616    public void addRequest(WMSRequest request) {
617        requestQueueLock.lock();
618        try {
619
620            if (cache != null) {
621                ProjectionBounds b = getBounds(request);
622                // Checking for exact match is fast enough, no need to do it in separated thread
623                request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
624                if (request.isPrecacheOnly() && request.hasExactMatch())
625                    return; // We already have this tile cached
626            }
627
628            if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
629                requestQueue.add(request);
630                if (request.getPrecacheTask() != null) {
631                    request.getPrecacheTask().totalCount++;
632                }
633                queueEmpty.signalAll();
634            }
635        } finally {
636            requestQueueLock.unlock();
637        }
638    }
639
640    public boolean requestIsVisible(WMSRequest request) {
641        return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
642    }
643
644    private void gatherFinishedRequests() {
645        requestQueueLock.lock();
646        try {
647            for (WMSRequest request: finishedRequests) {
648                GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
649                if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
650                    img.changeImage(request.getState(), request.getImage());
651                }
652            }
653        } finally {
654            requestQueueLock.unlock();
655            finishedRequests.clear();
656        }
657    }
658
659    public class DownloadAction extends AbstractAction {
660        public DownloadAction() {
661            super(tr("Download visible tiles"));
662        }
663        @Override
664        public void actionPerformed(ActionEvent ev) {
665            if (zoomIsTooBig()) {
666                JOptionPane.showMessageDialog(
667                        Main.parent,
668                        tr("The requested area is too big. Please zoom in a little, or change resolution"),
669                        tr("Error"),
670                        JOptionPane.ERROR_MESSAGE
671                        );
672            } else {
673                downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
674            }
675        }
676    }
677
678    /**
679     * Finds the most suitable resolution for the current zoom level, but prefers
680     * higher resolutions. Snaps to values defined in snapLevels.
681     * @return
682     */
683    private static double getBestZoom() {
684        // not sure why getDist100Pixel returns values corresponding to
685        // the snapLevels, which are in meters per pixel. It works, though.
686        double dist = Main.map.mapView.getDist100Pixel();
687        for(int i = snapLevels.length-2; i >= 0; i--) {
688            if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
689                return snapLevels[i+1];
690        }
691        return snapLevels[0];
692    }
693
694    /**
695     * Updates the given layer’s resolution settings to the current zoom level. Does
696     * not update existing tiles, only new ones will be subject to the new settings.
697     *
698     * @param layer
699     * @param snap  Set to true if the resolution should snap to certain values instead of
700     *              matching the current zoom level perfectly
701     */
702    private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
703        if(snap) {
704            layer.resolution = getBestZoom();
705            layer.resolutionText = MapView.getDistText(layer.resolution);
706        } else {
707            layer.resolution = Main.map.mapView.getDist100Pixel();
708            layer.resolutionText = Main.map.mapView.getDist100PixelText();
709        }
710        layer.info.setPixelPerDegree(layer.getPPD());
711    }
712
713    /**
714     * Updates the given layer’s resolution settings to the current zoom level and
715     * updates existing tiles. If round is true, tiles will be updated gradually, if
716     * false they will be removed instantly (and redrawn only after the new resolution
717     * image has been loaded).
718     * @param layer
719     * @param snap  Set to true if the resolution should snap to certain values instead of
720     *              matching the current zoom level perfectly
721     */
722    private static void changeResolution(WMSLayer layer, boolean snap) {
723        updateResolutionSetting(layer, snap);
724
725        layer.settingsChanged = true;
726
727        // Don’t move tiles off screen when the resolution is rounded. This
728        // prevents some flickering when zooming with auto-resolution enabled
729        // and instead gradually updates each tile.
730        if(!snap) {
731            for(int x = 0; x<layer.dax; ++x) {
732                for(int y = 0; y<layer.day; ++y) {
733                    layer.images[x][y].changePosition(-1, -1);
734                }
735            }
736        }
737    }
738
739    public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
740        
741        /**
742         * Constructs a new {@code ChangeResolutionAction}
743         */
744        public ChangeResolutionAction() {
745            super(tr("Change resolution"));
746        }
747
748        @Override
749        public void actionPerformed(ActionEvent ev) {
750            List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
751            for (Layer l: layers) {
752                changeResolution((WMSLayer) l, false);
753            }
754            Main.map.mapView.repaint();
755        }
756
757        @Override
758        public boolean supportLayers(List<Layer> layers) {
759            for (Layer l: layers) {
760                if (!(l instanceof WMSLayer))
761                    return false;
762            }
763            return true;
764        }
765
766        @Override
767        public Component createMenuComponent() {
768            return new JMenuItem(this);
769        }
770
771        @Override
772        public boolean equals(Object obj) {
773            return obj instanceof ChangeResolutionAction;
774        }
775    }
776
777    public class ReloadErrorTilesAction extends AbstractAction {
778        public ReloadErrorTilesAction() {
779            super(tr("Reload erroneous tiles"));
780        }
781        @Override
782        public void actionPerformed(ActionEvent ev) {
783            // Delete small files, because they're probably blank tiles.
784            // See #2307
785            cache.cleanSmallFiles(4096);
786
787            for (int x = 0; x < dax; ++x) {
788                for (int y = 0; y < day; ++y) {
789                    GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
790                    if(img.getState() == State.FAILED){
791                        addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
792                    }
793                }
794            }
795        }
796    }
797
798    public class ToggleAlphaAction extends AbstractAction implements LayerAction {
799        public ToggleAlphaAction() {
800            super(tr("Alpha channel"));
801        }
802        @Override
803        public void actionPerformed(ActionEvent ev) {
804            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
805            boolean alphaChannel = checkbox.isSelected();
806            PROP_ALPHA_CHANNEL.put(alphaChannel);
807
808            // clear all resized cached instances and repaint the layer
809            for (int x = 0; x < dax; ++x) {
810                for (int y = 0; y < day; ++y) {
811                    GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
812                    img.flushedResizedCachedInstance();
813                }
814            }
815            Main.map.mapView.repaint();
816        }
817        @Override
818        public Component createMenuComponent() {
819            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
820            item.setSelected(PROP_ALPHA_CHANNEL.get());
821            return item;
822        }
823        @Override
824        public boolean supportLayers(List<Layer> layers) {
825            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
826        }
827    }
828
829
830    public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
831        public ToggleAutoResolutionAction() {
832            super(tr("Automatically change resolution"));
833        }
834
835        @Override
836        public void actionPerformed(ActionEvent ev) {
837            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
838            autoResolutionEnabled = checkbox.isSelected();
839        }
840
841        @Override
842        public Component createMenuComponent() {
843            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
844            item.setSelected(autoResolutionEnabled);
845            return item;
846        }
847
848        @Override
849        public boolean supportLayers(List<Layer> layers) {
850            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
851        }
852    }
853
854    /**
855     * This action will add a WMS layer menu entry with the current WMS layer
856     * URL and name extended by the current resolution.
857     * When using the menu entry again, the WMS cache will be used properly.
858     */
859    public class BookmarkWmsAction extends AbstractAction {
860        public BookmarkWmsAction() {
861            super(tr("Set WMS Bookmark"));
862        }
863        @Override
864        public void actionPerformed(ActionEvent ev) {
865            ImageryLayerInfo.addLayer(new ImageryInfo(info));
866        }
867    }
868
869    private class StartStopAction extends AbstractAction implements LayerAction {
870
871        public StartStopAction() {
872            super(tr("Automatic downloading"));
873        }
874
875        @Override
876        public Component createMenuComponent() {
877            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
878            item.setSelected(autoDownloadEnabled);
879            return item;
880        }
881
882        @Override
883        public boolean supportLayers(List<Layer> layers) {
884            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
885        }
886
887        @Override
888        public void actionPerformed(ActionEvent e) {
889            autoDownloadEnabled = !autoDownloadEnabled;
890            if (autoDownloadEnabled) {
891                for (int x = 0; x < dax; ++x) {
892                    for (int y = 0; y < day; ++y) {
893                        GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
894                        if(img.getState() == State.NOT_IN_CACHE){
895                            addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
896                        }
897                    }
898                }
899                Main.map.mapView.repaint();
900            }
901        }
902    }
903
904    private class ZoomToNativeResolution extends AbstractAction {
905
906        public ZoomToNativeResolution() {
907            super(tr("Zoom to native resolution"));
908        }
909
910        @Override
911        public void actionPerformed(ActionEvent e) {
912            Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
913        }
914
915    }
916
917    private void cancelGrabberThreads(boolean wait) {
918        requestQueueLock.lock();
919        try {
920            canceled = true;
921            for (Grabber grabber: grabbers) {
922                grabber.cancel();
923            }
924            queueEmpty.signalAll();
925        } finally {
926            requestQueueLock.unlock();
927        }
928        if (wait) {
929            for (Thread t: grabberThreads) {
930                try {
931                    t.join();
932                } catch (InterruptedException e) {
933                    Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");
934                }
935            }
936        }
937    }
938
939    private void startGrabberThreads() {
940        int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
941        requestQueueLock.lock();
942        try {
943            canceled = false;
944            grabbers.clear();
945            grabberThreads.clear();
946            for (int i=0; i<threadCount; i++) {
947                Grabber grabber = getGrabber(i == 0 && threadCount > 1);
948                grabbers.add(grabber);
949                Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
950                t.setDaemon(true);
951                t.start();
952                grabberThreads.add(t);
953            }
954        } finally {
955            requestQueueLock.unlock();
956        }
957    }
958
959    @Override
960    public boolean isChanged() {
961        requestQueueLock.lock();
962        try {
963            return !finishedRequests.isEmpty() || settingsChanged;
964        } finally {
965            requestQueueLock.unlock();
966        }
967    }
968
969    @Override
970    public void preferenceChanged(PreferenceChangeEvent event) {
971        if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {
972            cancelGrabberThreads(true);
973            startGrabberThreads();
974        } else if (
975                event.getKey().equals(PROP_OVERLAP.getKey())
976                || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
977                || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
978            for (int i=0; i<images.length; i++) {
979                for (int k=0; k<images[i].length; k++) {
980                    images[i][k] = new GeorefImage(this);
981                }
982            }
983
984            settingsChanged = true;
985        }
986    }
987
988    protected Grabber getGrabber(boolean localOnly) {
989        if (getInfo().getImageryType() == ImageryType.HTML)
990            return new HTMLGrabber(Main.map.mapView, this, localOnly);
991        else if (getInfo().getImageryType() == ImageryType.WMS)
992            return new WMSGrabber(Main.map.mapView, this, localOnly);
993        else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
994    }
995
996    public ProjectionBounds getBounds(WMSRequest request) {
997        ProjectionBounds result = new ProjectionBounds(
998                getEastNorth(request.getXIndex(), request.getYIndex()),
999                getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
1000
1001        if (WMSLayer.PROP_OVERLAP.get()) {
1002            double eastSize =  result.maxEast - result.minEast;
1003            double northSize =  result.maxNorth - result.minNorth;
1004
1005            double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
1006            double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
1007
1008            result = new ProjectionBounds(result.getMin(),
1009                    new EastNorth(result.maxEast + eastCoef * eastSize,
1010                            result.maxNorth + northCoef * northSize));
1011        }
1012        return result;
1013    }
1014
1015    @Override
1016    public boolean isProjectionSupported(Projection proj) {
1017        List<String> serverProjections = info.getServerProjections();
1018        return serverProjections.contains(proj.toCode().toUpperCase())
1019                || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
1020                || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
1021    }
1022
1023    @Override
1024    public String nameSupportedProjections() {
1025        StringBuilder res = new StringBuilder();
1026        for (String p : info.getServerProjections()) {
1027            if (res.length() > 0) {
1028                res.append(", ");
1029            }
1030            res.append(p);
1031        }
1032        return tr("Supported projections are: {0}", res);
1033    }
1034
1035    @Override
1036    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
1037        boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
1038        Main.map.repaint(done ? 0 : 100);
1039        return !done;
1040    }
1041
1042    @Override
1043    public void writeExternal(ObjectOutput out) throws IOException {
1044        out.writeInt(serializeFormatVersion);
1045        out.writeInt(dax);
1046        out.writeInt(day);
1047        out.writeInt(imageSize);
1048        out.writeDouble(info.getPixelPerDegree());
1049        out.writeObject(info.getName());
1050        out.writeObject(info.getExtendedUrl());
1051        out.writeObject(images);
1052    }
1053
1054    @Override
1055    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
1056        int sfv = in.readInt();
1057        if (sfv != serializeFormatVersion)
1058            throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
1059        autoDownloadEnabled = false;
1060        dax = in.readInt();
1061        day = in.readInt();
1062        imageSize = in.readInt();
1063        info.setPixelPerDegree(in.readDouble());
1064        doSetName((String)in.readObject());
1065        info.setExtendedUrl((String)in.readObject());
1066        images = (GeorefImage[][])in.readObject();
1067
1068        for (GeorefImage[] imgs : images) {
1069            for (GeorefImage img : imgs) {
1070                if (img != null) {
1071                    img.setLayer(WMSLayer.this);
1072                }
1073            }
1074        }
1075
1076        settingsChanged = true;
1077        if (Main.isDisplayingMapView()) {
1078            Main.map.mapView.repaint();
1079        }
1080        if (cache != null) {
1081            cache.saveIndex();
1082            cache = null;
1083        }
1084    }
1085
1086    @Override
1087    public void onPostLoadFromFile() {
1088        if (info.getUrl() != null) {
1089            cache = new WmsCache(info.getUrl(), imageSize);
1090            startGrabberThreads();
1091        }
1092    }
1093
1094    @Override
1095    public boolean isSavable() {
1096        return true; // With WMSLayerExporter
1097    }
1098
1099    @Override
1100    public File createAndOpenSaveFileChooser() {
1101        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1102    }
1103}