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