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                    if (Main.map != null && Main.map.mapView != null) {
603                        Main.map.mapView.repaint();
604                    }
605                }
606            } finally {
607                requestQueueLock.unlock();
608            }
609        }
610    
611        public void addRequest(WMSRequest request) {
612            requestQueueLock.lock();
613            try {
614    
615                if (cache != null) {
616                    ProjectionBounds b = getBounds(request);
617                    // Checking for exact match is fast enough, no need to do it in separated thread
618                    request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
619                    if (request.isPrecacheOnly() && request.hasExactMatch())
620                        return; // We already have this tile cached
621                }
622    
623                if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
624                    requestQueue.add(request);
625                    if (request.getPrecacheTask() != null) {
626                        request.getPrecacheTask().totalCount++;
627                    }
628                    queueEmpty.signalAll();
629                }
630            } finally {
631                requestQueueLock.unlock();
632            }
633        }
634    
635        public boolean requestIsVisible(WMSRequest request) {
636            return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
637        }
638    
639        private void gatherFinishedRequests() {
640            requestQueueLock.lock();
641            try {
642                for (WMSRequest request: finishedRequests) {
643                    GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
644                    if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
645                        img.changeImage(request.getState(), request.getImage());
646                    }
647                }
648            } finally {
649                requestQueueLock.unlock();
650                finishedRequests.clear();
651            }
652        }
653    
654        public class DownloadAction extends AbstractAction {
655            public DownloadAction() {
656                super(tr("Download visible tiles"));
657            }
658            @Override
659            public void actionPerformed(ActionEvent ev) {
660                if (zoomIsTooBig()) {
661                    JOptionPane.showMessageDialog(
662                            Main.parent,
663                            tr("The requested area is too big. Please zoom in a little, or change resolution"),
664                            tr("Error"),
665                            JOptionPane.ERROR_MESSAGE
666                            );
667                } else {
668                    downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
669                }
670            }
671        }
672    
673        public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
674            public ChangeResolutionAction() {
675                super(tr("Change resolution"));
676            }
677    
678            private void changeResolution(WMSLayer layer) {
679                layer.resolution = Main.map.mapView.getDist100PixelText();
680                layer.info.setPixelPerDegree(layer.getPPD());
681                layer.settingsChanged = true;
682                for(int x = 0; x<layer.dax; ++x) {
683                    for(int y = 0; y<layer.day; ++y) {
684                        layer.images[x][y].changePosition(-1, -1);
685                    }
686                }
687            }
688    
689            @Override
690            public void actionPerformed(ActionEvent ev) {
691    
692                if (LayerListDialog.getInstance() == null)
693                    return;
694    
695                List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
696                for (Layer l: layers) {
697                    changeResolution((WMSLayer) l);
698                }
699                Main.map.mapView.repaint();
700            }
701    
702            @Override
703            public boolean supportLayers(List<Layer> layers) {
704                for (Layer l: layers) {
705                    if (!(l instanceof WMSLayer))
706                        return false;
707                }
708                return true;
709            }
710    
711            @Override
712            public Component createMenuComponent() {
713                return new JMenuItem(this);
714            }
715    
716            @Override
717            public boolean equals(Object obj) {
718                return obj instanceof ChangeResolutionAction;
719            }
720        }
721    
722        public class ReloadErrorTilesAction extends AbstractAction {
723            public ReloadErrorTilesAction() {
724                super(tr("Reload erroneous tiles"));
725            }
726            @Override
727            public void actionPerformed(ActionEvent ev) {
728                // Delete small files, because they're probably blank tiles.
729                // See https://josm.openstreetmap.de/ticket/2307
730                cache.cleanSmallFiles(4096);
731    
732                for (int x = 0; x < dax; ++x) {
733                    for (int y = 0; y < day; ++y) {
734                        GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
735                        if(img.getState() == State.FAILED){
736                            addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
737                        }
738                    }
739                }
740            }
741        }
742    
743        public class ToggleAlphaAction extends AbstractAction implements LayerAction {
744            public ToggleAlphaAction() {
745                super(tr("Alpha channel"));
746            }
747            @Override
748            public void actionPerformed(ActionEvent ev) {
749                JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
750                boolean alphaChannel = checkbox.isSelected();
751                PROP_ALPHA_CHANNEL.put(alphaChannel);
752    
753                // clear all resized cached instances and repaint the layer
754                for (int x = 0; x < dax; ++x) {
755                    for (int y = 0; y < day; ++y) {
756                        GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
757                        img.flushedResizedCachedInstance();
758                    }
759                }
760                Main.map.mapView.repaint();
761            }
762            @Override
763            public Component createMenuComponent() {
764                JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
765                item.setSelected(PROP_ALPHA_CHANNEL.get());
766                return item;
767            }
768            @Override
769            public boolean supportLayers(List<Layer> layers) {
770                return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
771            }
772        }
773    
774        /**
775         * This action will add a WMS layer menu entry with the current WMS layer
776         * URL and name extended by the current resolution.
777         * When using the menu entry again, the WMS cache will be used properly.
778         */
779        public class BookmarkWmsAction extends AbstractAction {
780            public BookmarkWmsAction() {
781                super(tr("Set WMS Bookmark"));
782            }
783            @Override
784            public void actionPerformed(ActionEvent ev) {
785                ImageryLayerInfo.addLayer(new ImageryInfo(info));
786            }
787        }
788    
789        private class StartStopAction extends AbstractAction implements LayerAction {
790    
791            public StartStopAction() {
792                super(tr("Automatic downloading"));
793            }
794    
795            @Override
796            public Component createMenuComponent() {
797                JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
798                item.setSelected(autoDownloadEnabled);
799                return item;
800            }
801    
802            @Override
803            public boolean supportLayers(List<Layer> layers) {
804                return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
805            }
806    
807            @Override
808            public void actionPerformed(ActionEvent e) {
809                autoDownloadEnabled = !autoDownloadEnabled;
810                if (autoDownloadEnabled) {
811                    for (int x = 0; x < dax; ++x) {
812                        for (int y = 0; y < day; ++y) {
813                            GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
814                            if(img.getState() == State.NOT_IN_CACHE){
815                                addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
816                            }
817                        }
818                    }
819                    Main.map.mapView.repaint();
820                }
821            }
822        }
823    
824        private class ZoomToNativeResolution extends AbstractAction {
825    
826            public ZoomToNativeResolution() {
827                super(tr("Zoom to native resolution"));
828            }
829    
830            @Override
831            public void actionPerformed(ActionEvent e) {
832                Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
833            }
834    
835        }
836    
837        private void cancelGrabberThreads(boolean wait) {
838            requestQueueLock.lock();
839            try {
840                canceled = true;
841                for (Grabber grabber: grabbers) {
842                    grabber.cancel();
843                }
844                queueEmpty.signalAll();
845            } finally {
846                requestQueueLock.unlock();
847            }
848            if (wait) {
849                for (Thread t: grabberThreads) {
850                    try {
851                        t.join();
852                    } catch (InterruptedException e) {
853                        // Shouldn't happen
854                        e.printStackTrace();
855                    }
856                }
857            }
858        }
859    
860        private void startGrabberThreads() {
861            int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
862            requestQueueLock.lock();
863            try {
864                canceled = false;
865                grabbers.clear();
866                grabberThreads.clear();
867                for (int i=0; i<threadCount; i++) {
868                    Grabber grabber = getGrabber(i == 0 && threadCount > 1);
869                    grabbers.add(grabber);
870                    Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
871                    t.setDaemon(true);
872                    t.start();
873                    grabberThreads.add(t);
874                }
875                this.workingThreadCount = grabbers.size();
876                this.threadCount = grabbers.size();
877            } finally {
878                requestQueueLock.unlock();
879            }
880        }
881    
882        @Override
883        public boolean isChanged() {
884            requestQueueLock.lock();
885            try {
886                return !finishedRequests.isEmpty() || settingsChanged;
887            } finally {
888                requestQueueLock.unlock();
889            }
890        }
891    
892        @Override
893        public void preferenceChanged(PreferenceChangeEvent event) {
894            if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
895                cancelGrabberThreads(true);
896                startGrabberThreads();
897            } else if (
898                    event.getKey().equals(PROP_OVERLAP.getKey())
899                    || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
900                    || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
901                for (int i=0; i<images.length; i++) {
902                    for (int k=0; k<images[i].length; k++) {
903                        images[i][k] = new GeorefImage(this);
904                    }
905                }
906    
907                settingsChanged = true;
908            }
909        }
910    
911        protected Grabber getGrabber(boolean localOnly) {
912            if (getInfo().getImageryType() == ImageryType.HTML)
913                return new HTMLGrabber(Main.map.mapView, this, localOnly);
914            else if (getInfo().getImageryType() == ImageryType.WMS)
915                return new WMSGrabber(Main.map.mapView, this, localOnly);
916            else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
917        }
918    
919        public ProjectionBounds getBounds(WMSRequest request) {
920            ProjectionBounds result = new ProjectionBounds(
921                    getEastNorth(request.getXIndex(), request.getYIndex()),
922                    getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
923    
924            if (WMSLayer.PROP_OVERLAP.get()) {
925                double eastSize =  result.maxEast - result.minEast;
926                double northSize =  result.maxNorth - result.minNorth;
927    
928                double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
929                double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
930    
931                result = new ProjectionBounds(result.getMin(),
932                        new EastNorth(result.maxEast + eastCoef * eastSize,
933                                result.maxNorth + northCoef * northSize));
934            }
935            return result;
936        }
937    
938        @Override
939        public boolean isProjectionSupported(Projection proj) {
940            List<String> serverProjections = info.getServerProjections();
941            return serverProjections.contains(proj.toCode().toUpperCase())
942                    || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
943                    || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
944        }
945    
946        @Override
947        public String nameSupportedProjections() {
948            String res = "";
949            for(String p : info.getServerProjections()) {
950                if(!res.isEmpty()) {
951                    res += ", ";
952                }
953                res += p;
954            }
955            return tr("Supported projections are: {0}", res);
956        }
957    
958        @Override
959        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
960            boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
961            Main.map.repaint(done ? 0 : 100);
962            return !done;
963        }
964    
965        @Override
966        public void writeExternal(ObjectOutput out) throws IOException {
967            out.writeInt(serializeFormatVersion);
968            out.writeInt(dax);
969            out.writeInt(day);
970            out.writeInt(imageSize);
971            out.writeDouble(info.getPixelPerDegree());
972            out.writeObject(info.getName());
973            out.writeObject(info.getExtendedUrl());
974            out.writeObject(images);
975        }
976    
977        @Override
978        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
979            int sfv = in.readInt();
980            if (sfv != serializeFormatVersion)
981                throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
982            autoDownloadEnabled = false;
983            dax = in.readInt();
984            day = in.readInt();
985            imageSize = in.readInt();
986            info.setPixelPerDegree(in.readDouble());
987            doSetName((String)in.readObject());
988            info.setExtendedUrl((String)in.readObject());
989            images = (GeorefImage[][])in.readObject();
990    
991            for (GeorefImage[] imgs : images) {
992                for (GeorefImage img : imgs) {
993                    if (img != null) {
994                        img.setLayer(WMSLayer.this);
995                    }
996                }
997            }
998    
999            settingsChanged = true;
1000            if (Main.isDisplayingMapView()) {
1001                Main.map.mapView.repaint();
1002            }
1003            if (cache != null) {
1004                cache.saveIndex();
1005                cache = null;
1006            }
1007        }
1008    
1009        @Override
1010        public void onPostLoadFromFile() {
1011            if (info.getUrl() != null) {
1012                cache = new WmsCache(info.getUrl(), imageSize);
1013                startGrabberThreads();
1014            }
1015        }
1016    
1017        @Override
1018        public boolean isSavable() {
1019            return true; // With WMSLayerExporter
1020        }
1021    
1022        @Override
1023        public File createAndOpenSaveFileChooser() {
1024            return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1025        }
1026    }