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