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            super.destroy();
260            cancelGrabberThreads(false);
261            Main.pref.removePreferenceChangeListener(this);
262            if (cache != null) {
263                cache.saveIndex();
264            }
265        }
266    
267        public void initializeImages() {
268            GeorefImage[][] old = images;
269            images = new GeorefImage[dax][day];
270            if (old != null) {
271                for (int i=0; i<old.length; i++) {
272                    for (int k=0; k<old[i].length; k++) {
273                        GeorefImage o = old[i][k];
274                        images[modulo(o.getXIndex(),dax)][modulo(o.getYIndex(),day)] = old[i][k];
275                    }
276                }
277            }
278            for(int x = 0; x<dax; ++x) {
279                for(int y = 0; y<day; ++y) {
280                    if (images[x][y] == null) {
281                        images[x][y]= new GeorefImage(this);
282                    }
283                }
284            }
285        }
286    
287        @Override public ImageryInfo getInfo() {
288            return info;
289        }
290    
291        @Override public String getToolTipText() {
292            if(autoDownloadEnabled)
293                return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolution);
294            else
295                return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolution);
296        }
297    
298        private int modulo (int a, int b) {
299            return a % b >= 0 ? a%b : a%b+b;
300        }
301    
302        private boolean zoomIsTooBig() {
303            //don't download when it's too outzoomed
304            return info.getPixelPerDegree() / getPPD() > minZoom;
305        }
306    
307        @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
308            if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
309    
310            settingsChanged = false;
311    
312            ProjectionBounds bounds = mv.getProjectionBounds();
313            bminx= getImageXIndex(bounds.minEast);
314            bminy= getImageYIndex(bounds.minNorth);
315            bmaxx= getImageXIndex(bounds.maxEast);
316            bmaxy= getImageYIndex(bounds.maxNorth);
317    
318            leftEdge = (int)(bounds.minEast * getPPD());
319            bottomEdge = (int)(bounds.minNorth * getPPD());
320    
321            if (zoomIsTooBig()) {
322                for(int x = 0; x<images.length; ++x) {
323                    for(int y = 0; y<images[0].length; ++y) {
324                        GeorefImage image = images[x][y];
325                        image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
326                    }
327                }
328            } else {
329                downloadAndPaintVisible(g, mv, false);
330            }
331    
332            attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
333    
334        }
335    
336        @Override
337        public void setOffset(double dx, double dy) {
338            super.setOffset(dx, dy);
339            settingsChanged = true;
340        }
341    
342        public int getImageXIndex(double coord) {
343            return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
344        }
345    
346        public int getImageYIndex(double coord) {
347            return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
348        }
349    
350        public int getImageX(int imageIndex) {
351            return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
352        }
353    
354        public int getImageY(int imageIndex) {
355            return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
356        }
357    
358        public int getImageWidth(int xIndex) {
359            return getImageX(xIndex + 1) - getImageX(xIndex);
360        }
361    
362        public int getImageHeight(int yIndex) {
363            return getImageY(yIndex + 1) - getImageY(yIndex);
364        }
365    
366        /**
367         *
368         * @return Size of image in original zoom
369         */
370        public int getBaseImageWidth() {
371            int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_EAST.get() * imageSize / 100) : 0;
372            return imageSize + overlap;
373        }
374    
375        /**
376         *
377         * @return Size of image in original zoom
378         */
379        public int getBaseImageHeight() {
380            int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_NORTH.get() * imageSize / 100) : 0;
381            return imageSize + overlap;
382        }
383    
384        public int getImageSize() {
385            return imageSize;
386        }
387    
388        public boolean isOverlapEnabled() {
389            return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0);
390        }
391    
392        /**
393         *
394         * @return When overlapping is enabled, return visible part of tile. Otherwise return original image
395         */
396        public BufferedImage normalizeImage(BufferedImage img) {
397            if (isOverlapEnabled()) {
398                BufferedImage copy = img;
399                img = new BufferedImage(imageSize, imageSize, copy.getType());
400                img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize,
401                        0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null);
402            }
403            return img;
404        }
405    
406        /**
407         *
408         * @param xIndex
409         * @param yIndex
410         * @return Real EastNorth of given tile. dx/dy is not counted in
411         */
412        public EastNorth getEastNorth(int xIndex, int yIndex) {
413            return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
414        }
415    
416        protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
417    
418            int newDax = dax;
419            int newDay = day;
420    
421            if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
422                newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
423            }
424    
425            if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
426                newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
427            }
428    
429            if (newDax != dax || newDay != day) {
430                dax = newDax;
431                day = newDay;
432                initializeImages();
433            }
434    
435            for(int x = bminx; x<=bmaxx; ++x) {
436                for(int y = bminy; y<=bmaxy; ++y){
437                    images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
438                }
439            }
440    
441            gatherFinishedRequests();
442            Set<ProjectionBounds> areaToCache = new HashSet<ProjectionBounds>();
443    
444            for(int x = bminx; x<=bmaxx; ++x) {
445                for(int y = bminy; y<=bmaxy; ++y){
446                    GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
447                    if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
448                        WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true);
449                        addRequest(request);
450                        areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
451                    } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
452                        WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false);
453                        addRequest(request);
454                        areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
455                    }
456                }
457            }
458            if (cache != null) {
459                cache.setAreaToCache(areaToCache);
460            }
461        }
462    
463        @Override public void visitBoundingBox(BoundingXYVisitor v) {
464            for(int x = 0; x<dax; ++x) {
465                for(int y = 0; y<day; ++y)
466                    if(images[x][y].getImage() != null){
467                        v.visit(images[x][y].getMin());
468                        v.visit(images[x][y].getMax());
469                    }
470            }
471        }
472    
473        @Override public Action[] getMenuEntries() {
474            return new Action[]{
475                    LayerListDialog.getInstance().createActivateLayerAction(this),
476                    LayerListDialog.getInstance().createShowHideLayerAction(),
477                    LayerListDialog.getInstance().createDeleteLayerAction(),
478                    SeparatorLayerAction.INSTANCE,
479                    new OffsetAction(),
480                    new LayerSaveAction(this),
481                    new LayerSaveAsAction(this),
482                    new BookmarkWmsAction(),
483                    SeparatorLayerAction.INSTANCE,
484                    new ZoomToNativeResolution(),
485                    new StartStopAction(),
486                    new ToggleAlphaAction(),
487                    new ChangeResolutionAction(),
488                    new ReloadErrorTilesAction(),
489                    new DownloadAction(),
490                    SeparatorLayerAction.INSTANCE,
491                    new LayerListPopup.InfoAction(this)
492            };
493        }
494    
495        public GeorefImage findImage(EastNorth eastNorth) {
496            int xIndex = getImageXIndex(eastNorth.east());
497            int yIndex = getImageYIndex(eastNorth.north());
498            GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
499            if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
500                return result;
501            else
502                return null;
503        }
504    
505        /**
506         *
507         * @param request
508         * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)
509         */
510        private int getRequestPriority(WMSRequest request) {
511            if (request.getPixelPerDegree() != info.getPixelPerDegree())
512                return -1;
513            if (bminx > request.getXIndex()
514                    || bmaxx < request.getXIndex()
515                    || bminy > request.getYIndex()
516                    || bmaxy < request.getYIndex())
517                return -1;
518    
519            MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
520            EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
521            int mouseX = getImageXIndex(cursorEastNorth.east());
522            int mouseY = getImageYIndex(cursorEastNorth.north());
523            int dx = request.getXIndex() - mouseX;
524            int dy = request.getYIndex() - mouseY;
525    
526            return 1 + dx * dx + dy * dy;
527        }
528    
529        private void sortRequests(boolean localOnly) {
530            Iterator<WMSRequest> it = requestQueue.iterator();
531            while (it.hasNext()) {
532                WMSRequest item = it.next();
533    
534                if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
535                    it.remove();
536                    continue;
537                }
538    
539                int priority = getRequestPriority(item);
540                if (priority == -1 && item.isPrecacheOnly()) {
541                    priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
542                }
543    
544                if (localOnly && !item.hasExactMatch()) {
545                    priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
546                }
547    
548                if (       priority == -1
549                        || finishedRequests.contains(item)
550                        || processingRequests.contains(item)) {
551                    it.remove();
552                } else {
553                    item.setPriority(priority);
554                }
555            }
556            Collections.sort(requestQueue);
557        }
558    
559        public WMSRequest getRequest(boolean localOnly) {
560            requestQueueLock.lock();
561            try {
562                workingThreadCount--;
563    
564                sortRequests(localOnly);
565                while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
566                    try {
567                        queueEmpty.await();
568                        sortRequests(localOnly);
569                    } catch (InterruptedException e) {
570                        // Shouldn't happen
571                    }
572                }
573    
574                workingThreadCount++;
575                if (canceled)
576                    return null;
577                else {
578                    WMSRequest request = requestQueue.remove(0);
579                    processingRequests.add(request);
580                    return request;
581                }
582    
583            } finally {
584                requestQueueLock.unlock();
585            }
586        }
587    
588        public void finishRequest(WMSRequest request) {
589            requestQueueLock.lock();
590            try {
591                PrecacheTask task = request.getPrecacheTask();
592                if (task != null) {
593                    task.processedCount++;
594                    if (!task.progressMonitor.isCanceled()) {
595                        task.progressMonitor.worked(1);
596                        task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
597                    }
598                }
599                processingRequests.remove(request);
600                if (request.getState() != null && !request.isPrecacheOnly()) {
601                    finishedRequests.add(request);
602                    Main.map.mapView.repaint();
603                }
604            } finally {
605                requestQueueLock.unlock();
606            }
607        }
608    
609        public void addRequest(WMSRequest request) {
610            requestQueueLock.lock();
611            try {
612    
613                if (cache != null) {
614                    ProjectionBounds b = getBounds(request);
615                    // Checking for exact match is fast enough, no need to do it in separated thread
616                    request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
617                    if (request.isPrecacheOnly() && request.hasExactMatch())
618                        return; // We already have this tile cached
619                }
620    
621                if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
622                    requestQueue.add(request);
623                    if (request.getPrecacheTask() != null) {
624                        request.getPrecacheTask().totalCount++;
625                    }
626                    queueEmpty.signalAll();
627                }
628            } finally {
629                requestQueueLock.unlock();
630            }
631        }
632    
633        public boolean requestIsVisible(WMSRequest request) {
634            return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
635        }
636    
637        private void gatherFinishedRequests() {
638            requestQueueLock.lock();
639            try {
640                for (WMSRequest request: finishedRequests) {
641                    GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
642                    if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
643                        img.changeImage(request.getState(), request.getImage());
644                    }
645                }
646            } finally {
647                requestQueueLock.unlock();
648                finishedRequests.clear();
649            }
650        }
651    
652        public class DownloadAction extends AbstractAction {
653            public DownloadAction() {
654                super(tr("Download visible tiles"));
655            }
656            @Override
657            public void actionPerformed(ActionEvent ev) {
658                if (zoomIsTooBig()) {
659                    JOptionPane.showMessageDialog(
660                            Main.parent,
661                            tr("The requested area is too big. Please zoom in a little, or change resolution"),
662                            tr("Error"),
663                            JOptionPane.ERROR_MESSAGE
664                            );
665                } else {
666                    downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
667                }
668            }
669        }
670    
671        public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
672            public ChangeResolutionAction() {
673                super(tr("Change resolution"));
674            }
675    
676            private void changeResolution(WMSLayer layer) {
677                layer.resolution = Main.map.mapView.getDist100PixelText();
678                layer.info.setPixelPerDegree(layer.getPPD());
679                layer.settingsChanged = true;
680                for(int x = 0; x<layer.dax; ++x) {
681                    for(int y = 0; y<layer.day; ++y) {
682                        layer.images[x][y].changePosition(-1, -1);
683                    }
684                }
685            }
686    
687            @Override
688            public void actionPerformed(ActionEvent ev) {
689    
690                if (LayerListDialog.getInstance() == null)
691                    return;
692    
693                List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
694                for (Layer l: layers) {
695                    changeResolution((WMSLayer) l);
696                }
697                Main.map.mapView.repaint();
698            }
699    
700            @Override
701            public boolean supportLayers(List<Layer> layers) {
702                for (Layer l: layers) {
703                    if (!(l instanceof WMSLayer))
704                        return false;
705                }
706                return true;
707            }
708    
709            @Override
710            public Component createMenuComponent() {
711                return new JMenuItem(this);
712            }
713    
714            @Override
715            public boolean equals(Object obj) {
716                return obj instanceof ChangeResolutionAction;
717            }
718        }
719    
720        public class ReloadErrorTilesAction extends AbstractAction {
721            public ReloadErrorTilesAction() {
722                super(tr("Reload erroneous tiles"));
723            }
724            @Override
725            public void actionPerformed(ActionEvent ev) {
726                // Delete small files, because they're probably blank tiles.
727                // See https://josm.openstreetmap.de/ticket/2307
728                cache.cleanSmallFiles(4096);
729    
730                for (int x = 0; x < dax; ++x) {
731                    for (int y = 0; y < day; ++y) {
732                        GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
733                        if(img.getState() == State.FAILED){
734                            addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
735                        }
736                    }
737                }
738            }
739        }
740    
741        public class ToggleAlphaAction extends AbstractAction implements LayerAction {
742            public ToggleAlphaAction() {
743                super(tr("Alpha channel"));
744            }
745            @Override
746            public void actionPerformed(ActionEvent ev) {
747                JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
748                boolean alphaChannel = checkbox.isSelected();
749                PROP_ALPHA_CHANNEL.put(alphaChannel);
750    
751                // clear all resized cached instances and repaint the layer
752                for (int x = 0; x < dax; ++x) {
753                    for (int y = 0; y < day; ++y) {
754                        GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
755                        img.flushedResizedCachedInstance();
756                    }
757                }
758                Main.map.mapView.repaint();
759            }
760            @Override
761            public Component createMenuComponent() {
762                JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
763                item.setSelected(PROP_ALPHA_CHANNEL.get());
764                return item;
765            }
766            @Override
767            public boolean supportLayers(List<Layer> layers) {
768                return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
769            }
770        }
771    
772        /**
773         * This action will add a WMS layer menu entry with the current WMS layer
774         * URL and name extended by the current resolution.
775         * When using the menu entry again, the WMS cache will be used properly.
776         */
777        public class BookmarkWmsAction extends AbstractAction {
778            public BookmarkWmsAction() {
779                super(tr("Set WMS Bookmark"));
780            }
781            @Override
782            public void actionPerformed(ActionEvent ev) {
783                ImageryLayerInfo.addLayer(new ImageryInfo(info));
784            }
785        }
786    
787        private class StartStopAction extends AbstractAction implements LayerAction {
788    
789            public StartStopAction() {
790                super(tr("Automatic downloading"));
791            }
792    
793            @Override
794            public Component createMenuComponent() {
795                JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
796                item.setSelected(autoDownloadEnabled);
797                return item;
798            }
799    
800            @Override
801            public boolean supportLayers(List<Layer> layers) {
802                return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
803            }
804    
805            @Override
806            public void actionPerformed(ActionEvent e) {
807                autoDownloadEnabled = !autoDownloadEnabled;
808                if (autoDownloadEnabled) {
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                            if(img.getState() == State.NOT_IN_CACHE){
813                                addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
814                            }
815                        }
816                    }
817                    Main.map.mapView.repaint();
818                }
819            }
820        }
821    
822        private class ZoomToNativeResolution extends AbstractAction {
823    
824            public ZoomToNativeResolution() {
825                super(tr("Zoom to native resolution"));
826            }
827    
828            @Override
829            public void actionPerformed(ActionEvent e) {
830                Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
831            }
832    
833        }
834    
835        private void cancelGrabberThreads(boolean wait) {
836            requestQueueLock.lock();
837            try {
838                canceled = true;
839                for (Grabber grabber: grabbers) {
840                    grabber.cancel();
841                }
842                queueEmpty.signalAll();
843            } finally {
844                requestQueueLock.unlock();
845            }
846            if (wait) {
847                for (Thread t: grabberThreads) {
848                    try {
849                        t.join();
850                    } catch (InterruptedException e) {
851                        // Shouldn't happen
852                        e.printStackTrace();
853                    }
854                }
855            }
856        }
857    
858        private void startGrabberThreads() {
859            int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
860            requestQueueLock.lock();
861            try {
862                canceled = false;
863                grabbers.clear();
864                grabberThreads.clear();
865                for (int i=0; i<threadCount; i++) {
866                    Grabber grabber = getGrabber(i == 0 && threadCount > 1);
867                    grabbers.add(grabber);
868                    Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
869                    t.setDaemon(true);
870                    t.start();
871                    grabberThreads.add(t);
872                }
873                this.workingThreadCount = grabbers.size();
874                this.threadCount = grabbers.size();
875            } finally {
876                requestQueueLock.unlock();
877            }
878        }
879    
880        @Override
881        public boolean isChanged() {
882            requestQueueLock.lock();
883            try {
884                return !finishedRequests.isEmpty() || settingsChanged;
885            } finally {
886                requestQueueLock.unlock();
887            }
888        }
889    
890        @Override
891        public void preferenceChanged(PreferenceChangeEvent event) {
892            if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
893                cancelGrabberThreads(true);
894                startGrabberThreads();
895            } else if (
896                    event.getKey().equals(PROP_OVERLAP.getKey())
897                    || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
898                    || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
899                for (int i=0; i<images.length; i++) {
900                    for (int k=0; k<images[i].length; k++) {
901                        images[i][k] = new GeorefImage(this);
902                    }
903                }
904    
905                settingsChanged = true;
906            }
907        }
908    
909        protected Grabber getGrabber(boolean localOnly) {
910            if (getInfo().getImageryType() == ImageryType.HTML)
911                return new HTMLGrabber(Main.map.mapView, this, localOnly);
912            else if (getInfo().getImageryType() == ImageryType.WMS)
913                return new WMSGrabber(Main.map.mapView, this, localOnly);
914            else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
915        }
916    
917        public ProjectionBounds getBounds(WMSRequest request) {
918            ProjectionBounds result = new ProjectionBounds(
919                    getEastNorth(request.getXIndex(), request.getYIndex()),
920                    getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
921    
922            if (WMSLayer.PROP_OVERLAP.get()) {
923                double eastSize =  result.maxEast - result.minEast;
924                double northSize =  result.maxNorth - result.minNorth;
925    
926                double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
927                double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
928    
929                result = new ProjectionBounds(result.getMin(),
930                        new EastNorth(result.maxEast + eastCoef * eastSize,
931                                result.maxNorth + northCoef * northSize));
932            }
933            return result;
934        }
935    
936        @Override
937        public boolean isProjectionSupported(Projection proj) {
938            List<String> serverProjections = info.getServerProjections();
939            return serverProjections.contains(proj.toCode().toUpperCase())
940                    || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
941                    || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
942        }
943    
944        @Override
945        public String nameSupportedProjections() {
946            String res = "";
947            for(String p : info.getServerProjections()) {
948                if(!res.isEmpty()) {
949                    res += ", ";
950                }
951                res += p;
952            }
953            return tr("Supported projections are: {0}", res);
954        }
955    
956        @Override
957        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
958            boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
959            Main.map.repaint(done ? 0 : 100);
960            return !done;
961        }
962    
963        @Override
964        public void writeExternal(ObjectOutput out) throws IOException {
965            out.writeInt(serializeFormatVersion);
966            out.writeInt(dax);
967            out.writeInt(day);
968            out.writeInt(imageSize);
969            out.writeDouble(info.getPixelPerDegree());
970            out.writeObject(info.getName());
971            out.writeObject(info.getExtendedUrl());
972            out.writeObject(images);
973        }
974    
975        @Override
976        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
977            int sfv = in.readInt();
978            if (sfv != serializeFormatVersion)
979                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
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    }