Index: trunk/.classpath
===================================================================
--- trunk/.classpath	(revision 4744)
+++ trunk/.classpath	(revision 4745)
@@ -22,4 +22,5 @@
 	<classpathentry kind="lib" path="test/lib/unitils-core/ognl-2.6.9.jar"/>
 	<classpathentry kind="lib" path="test/lib/unitils-core/unitils-core-3.1.jar"/>
+	<classpathentry kind="lib" path="test/lib/fest/debug-1.0.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
Index: trunk/src/org/openstreetmap/josm/data/imagery/WmsCache.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WmsCache.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WmsCache.java	(revision 4745)
@@ -62,14 +62,15 @@
         final double north;
         final ProjectionBounds bounds;
+        final String filename;
 
         long lastUsed;
         long lastModified;
-        String filename;
-
-        CacheEntry(double pixelPerDegree, double east, double north, int tileSize) {
+
+        CacheEntry(double pixelPerDegree, double east, double north, int tileSize, String filename) {
             this.pixelPerDegree = pixelPerDegree;
             this.east = east;
             this.north = north;
             this.bounds = new ProjectionBounds(east, north, east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree);
+            this.filename = filename;
         }
     }
@@ -201,7 +202,6 @@
                 ProjectionEntries projection = getProjectionEntries(projectionType.getName(), projectionType.getCacheDirectory());
                 for (EntryType entry: projectionType.getEntry()) {
-                    CacheEntry ce = new CacheEntry(entry.getPixelPerDegree(), entry.getEast(), entry.getNorth(), tileSize);
+                    CacheEntry ce = new CacheEntry(entry.getPixelPerDegree(), entry.getEast(), entry.getNorth(), tileSize, entry.getFilename());
                     ce.lastUsed = entry.getLastUsed().getTimeInMillis();
-                    ce.filename = entry.getFilename();
                     ce.lastModified = entry.getLastModified().getTimeInMillis();
                     projection.entries.add(ce);
@@ -315,25 +315,34 @@
     }
 
+
     private BufferedImage loadImage(ProjectionEntries projectionEntries, CacheEntry entry) throws IOException {
-        entry.lastUsed = System.currentTimeMillis();
-
-        SoftReference<BufferedImage> memCache = memoryCache.get(entry);
-        if (memCache != null) {
-            BufferedImage result = memCache.get();
-            if (result != null)
+
+        synchronized (this) {
+            entry.lastUsed = System.currentTimeMillis();
+
+            SoftReference<BufferedImage> memCache = memoryCache.get(entry);
+            if (memCache != null) {
+                BufferedImage result = memCache.get();
+                if (result != null)
+                    return result;
+            }
+        }
+
+        try {
+            // Reading can't be in synchronized section, it's too slow
+            BufferedImage result = ImageIO.read(getImageFile(projectionEntries, entry));
+            synchronized (this) {
+                if (result == null) {
+                    projectionEntries.entries.remove(entry);
+                    totalFileSizeDirty = true;
+                }
                 return result;
-        }
-
-        try {
-            BufferedImage result = ImageIO.read(getImageFile(projectionEntries, entry));
-            if (result == null) {
+            }
+        } catch (IOException e) {
+            synchronized (this) {
                 projectionEntries.entries.remove(entry);
                 totalFileSizeDirty = true;
-            }
-            return result;
-        } catch (IOException e) {
-            projectionEntries.entries.remove(entry);
-            totalFileSizeDirty = true;
-            throw e;
+                throw e;
+            }
         }
     }
@@ -347,10 +356,19 @@
     }
 
-    public synchronized BufferedImage getExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
+    public synchronized boolean hasExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
         ProjectionEntries projectionEntries = getProjectionEntries(projection);
         CacheEntry entry = findEntry(projectionEntries, pixelPerDegree, east, north);
+        return (entry != null);
+    }
+
+    public BufferedImage getExactMatch(Projection projection, double pixelPerDegree, double east, double north) {
+        CacheEntry entry = null;
+        ProjectionEntries projectionEntries = null;
+        synchronized (this) {
+            projectionEntries = getProjectionEntries(projection);
+            entry = findEntry(projectionEntries, pixelPerDegree, east, north);
+        }
         if (entry != null) {
             try {
-                entry.lastUsed = System.currentTimeMillis();
                 return loadImage(projectionEntries, entry);
             } catch (IOException e) {
@@ -363,32 +381,38 @@
     }
 
-    public synchronized BufferedImage getPartialMatch(Projection projection, double pixelPerDegree, double east, double north) {
-        List<CacheEntry> matches = new ArrayList<WmsCache.CacheEntry>();
-
-        double minPPD = pixelPerDegree / 5;
-        double maxPPD = pixelPerDegree * 5;
-        ProjectionEntries projectionEntries = getProjectionEntries(projection);
-
-        ProjectionBounds bounds = new ProjectionBounds(east, north,
-                east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree);
-
-        //TODO Do not load tile if it is completely overlapped by other tile with better ppd
-        for (CacheEntry entry: projectionEntries.entries) {
-            if (entry.pixelPerDegree >= minPPD && entry.pixelPerDegree <= maxPPD && entry.bounds.intersects(bounds)) {
-                entry.lastUsed = System.currentTimeMillis();
-                matches.add(entry);
-            }
-        }
-
-        if (matches.isEmpty())
-            return null;
-
-
-        Collections.sort(matches, new Comparator<CacheEntry>() {
-            @Override
-            public int compare(CacheEntry o1, CacheEntry o2) {
-                return Double.compare(o2.pixelPerDegree, o1.pixelPerDegree);
-            }
-        });
+    public  BufferedImage getPartialMatch(Projection projection, double pixelPerDegree, double east, double north) {
+        ProjectionEntries projectionEntries;
+        List<CacheEntry> matches;
+        synchronized (this) {
+            matches = new ArrayList<WmsCache.CacheEntry>();
+
+            double minPPD = pixelPerDegree / 5;
+            double maxPPD = pixelPerDegree * 5;
+            projectionEntries = getProjectionEntries(projection);
+
+            double size2 = tileSize / pixelPerDegree;
+            double border = tileSize * 0.01; // Make sure not to load neighboring tiles that intersects this tile only slightly
+            ProjectionBounds bounds = new ProjectionBounds(east + border, north + border,
+                    east + size2 - border, north + size2 - border);
+
+            //TODO Do not load tile if it is completely overlapped by other tile with better ppd
+            for (CacheEntry entry: projectionEntries.entries) {
+                if (entry.pixelPerDegree >= minPPD && entry.pixelPerDegree <= maxPPD && entry.bounds.intersects(bounds)) {
+                    entry.lastUsed = System.currentTimeMillis();
+                    matches.add(entry);
+                }
+            }
+
+            if (matches.isEmpty())
+                return null;
+
+
+            Collections.sort(matches, new Comparator<CacheEntry>() {
+                @Override
+                public int compare(CacheEntry o1, CacheEntry o2) {
+                    return Double.compare(o2.pixelPerDegree, o1.pixelPerDegree);
+                }
+            });
+        }
 
         //TODO Use alpha layer only when enabled on wms layer
@@ -396,10 +420,12 @@
         Graphics2D g = result.createGraphics();
 
+
         boolean drawAtLeastOnce = false;
+        Map<CacheEntry, SoftReference<BufferedImage>> localCache = new HashMap<WmsCache.CacheEntry, SoftReference<BufferedImage>>();
         for (CacheEntry ce: matches) {
             BufferedImage img;
             try {
                 img = loadImage(projectionEntries, ce);
-                memoryCache.put(ce, new SoftReference<BufferedImage>(img));
+                localCache.put(ce, new SoftReference<BufferedImage>(img));
             } catch (IOException e) {
                 continue;
@@ -418,7 +444,10 @@
         }
 
-        if (drawAtLeastOnce)
+        if (drawAtLeastOnce) {
+            synchronized (this) {
+                memoryCache.putAll(localCache);
+            }
             return result;
-        else
+        } else
             return null;
     }
@@ -461,5 +490,5 @@
 
     /**
-     * 
+     *
      * @param img Used only when overlapping is used, when not used, used raw from imageData
      * @param imageData
@@ -475,7 +504,4 @@
         File imageFile;
         if (entry == null) {
-            entry = new CacheEntry(pixelPerDegree, east, north, tileSize);
-            entry.lastUsed = System.currentTimeMillis();
-            entry.lastModified = entry.lastUsed;
 
             String mimeType;
@@ -485,5 +511,7 @@
                 mimeType = URLConnection.guessContentTypeFromStream(imageData);
             }
-            entry.filename = generateFileName(projectionEntries, pixelPerDegree, projection, east, north, mimeType);
+            entry = new CacheEntry(pixelPerDegree, east, north, tileSize,generateFileName(projectionEntries, pixelPerDegree, projection, east, north, mimeType));
+            entry.lastUsed = System.currentTimeMillis();
+            entry.lastModified = entry.lastUsed;
             projectionEntries.entries.add(entry);
             imageFile = getImageFile(projectionEntries, entry);
Index: trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 4745)
@@ -24,4 +24,5 @@
 import java.awt.geom.Rectangle2D;
 import java.io.File;
+import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -39,5 +40,7 @@
 import javax.swing.AbstractAction;
 import javax.swing.Action;
+import javax.swing.DefaultComboBoxModel;
 import javax.swing.Icon;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JFileChooser;
@@ -57,4 +60,5 @@
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.AbstractMergeAction.LayerListCellRenderer;
 import org.openstreetmap.josm.actions.RenameLayerAction;
 import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTaskList;
@@ -80,4 +84,5 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
 import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
 import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
@@ -86,6 +91,9 @@
 import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressTaskId;
+import org.openstreetmap.josm.gui.progress.ProgressTaskIds;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
 import org.openstreetmap.josm.io.JpgImporter;
+import org.openstreetmap.josm.io.OsmTransferException;
 import org.openstreetmap.josm.tools.AudioUtil;
 import org.openstreetmap.josm.tools.DateUtils;
@@ -96,4 +104,5 @@
 import org.openstreetmap.josm.tools.Utils;
 import org.openstreetmap.josm.tools.WindowGeometry;
+import org.xml.sax.SAXException;
 
 public class GpxLayer extends Layer {
@@ -297,4 +306,5 @@
                 new ConvertToDataLayerAction(),
                 new DownloadAlongTrackAction(),
+                new DownloadWmsAlongTrackAction(),
                 SeparatorLayerAction.INSTANCE,
                 new ChooseTrackVisibilityAction(),
@@ -1245,4 +1255,5 @@
         }
 
+
         /**
          * Area "a" contains the hull that we would like to download data for. however we
@@ -1309,4 +1320,105 @@
                     }
                     );
+        }
+    }
+
+
+    public class DownloadWmsAlongTrackAction extends AbstractAction {
+        public DownloadWmsAlongTrackAction() {
+            super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+
+            final List<LatLon> points = new ArrayList<LatLon>();
+
+            for (GpxTrack trk : data.tracks) {
+                for (GpxTrackSegment segment : trk.getSegments()) {
+                    for (WayPoint p : segment.getWayPoints()) {
+                        points.add(p.getCoor());
+                    }
+                }
+            }
+            for (WayPoint p : data.waypoints) {
+                points.add(p.getCoor());
+            }
+
+
+            final WMSLayer layer = askWMSLayer();
+            if (layer != null) {
+                PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) {
+
+                    private PrecacheTask precacheTask;
+
+                    @Override
+                    protected void realRun() throws SAXException, IOException, OsmTransferException {
+                        precacheTask = new PrecacheTask(progressMonitor);
+                        layer.downloadAreaToCache(precacheTask, points, 0, 0);
+                        while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) {
+                            synchronized (this) {
+                                try {
+                                    wait(200);
+                                } catch (InterruptedException e) {
+                                    e.printStackTrace();
+                                }
+                            }
+                        }
+                    }
+
+                    @Override
+                    protected void finish() {
+                    }
+
+                    @Override
+                    protected void cancel() {
+                        precacheTask.cancel();
+                    }
+
+                    @Override
+                    public ProgressTaskId canRunInBackground() {
+                        return ProgressTaskIds.PRECACHE_WMS;
+                    }
+                };
+                Main.worker.execute(task);
+            }
+
+
+        }
+
+        protected WMSLayer askWMSLayer() {
+            List<WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class);
+
+            if (targetLayers.isEmpty()) {
+                warnNoImageryLayers();
+                return null;
+            }
+
+            JComboBox layerList = new JComboBox();
+            layerList.setRenderer(new LayerListCellRenderer());
+            layerList.setModel(new DefaultComboBoxModel(targetLayers.toArray()));
+            layerList.setSelectedIndex(0);
+
+            JPanel pnl = new JPanel();
+            pnl.setLayout(new GridBagLayout());
+            pnl.add(new JLabel(tr("Please select the imagery layer.")), GBC.eol());
+            pnl.add(layerList, GBC.eol());
+
+            ExtendedDialog ed = new ExtendedDialog(Main.parent,
+                    tr("Select imagery layer"),
+                    new String[] { tr("Download"), tr("Cancel") });
+            ed.setButtonIcons(new String[] { "dialogs/down", "cancel" });
+            ed.setContent(pnl);
+            ed.showDialog();
+            if (ed.getValue() != 1)
+                return null;
+
+            WMSLayer targetLayer = (WMSLayer) layerList.getSelectedItem();
+            return targetLayer;
+        }
+
+        protected void warnNoImageryLayers() {
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("There are no imagery layers."),
+                    tr("No imagery layers"), JOptionPane.WARNING_MESSAGE);
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 4745)
@@ -8,4 +8,5 @@
 import java.awt.Graphics2D;
 import java.awt.Image;
+import java.awt.Point;
 import java.awt.event.ActionEvent;
 import java.awt.event.MouseAdapter;
@@ -45,4 +46,5 @@
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.imagery.GeorefImage;
 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
@@ -61,4 +63,5 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.io.imagery.Grabber;
 import org.openstreetmap.josm.io.imagery.HTMLGrabber;
@@ -67,4 +70,5 @@
 import org.openstreetmap.josm.tools.ImageProvider;
 
+
 /**
  * This is a layer that grabs the current screen from an WMS server. The data
@@ -72,4 +76,27 @@
  */
 public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener {
+
+    public static class PrecacheTask {
+        private final ProgressMonitor progressMonitor;
+        private volatile int totalCount;
+        private volatile int processedCount;
+        private volatile boolean isCancelled;
+
+        public PrecacheTask(ProgressMonitor progressMonitor) {
+            this.progressMonitor = progressMonitor;
+        }
+
+        boolean isFinished() {
+            return totalCount == processedCount;
+        }
+
+        public int getTotalCount() {
+            return totalCount;
+        }
+
+        public void cancel() {
+            isCancelled = true;
+        }
+    }
 
     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
@@ -159,4 +186,5 @@
         }
 
+
         Main.pref.addPreferenceChangeListener(this);
 
@@ -205,4 +233,29 @@
     public boolean hasAutoDownload(){
         return autoDownloadEnabled;
+    }
+
+    public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
+        Set<Point> requestedTiles = new HashSet<Point>();
+        for (LatLon point: points) {
+            EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));
+            EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));
+            int minX = getImageXIndex(minEn.east());
+            int maxX = getImageXIndex(maxEn.east());
+            int minY = getImageYIndex(minEn.north());
+            int maxY = getImageYIndex(maxEn.north());
+
+            for (int x=minX; x<=maxX; x++) {
+                for (int y=minY; y<=maxY; y++) {
+                    requestedTiles.add(new Point(x, y));
+                }
+            }
+        }
+
+        for (Point p: requestedTiles) {
+            addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));
+        }
+
+        precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());
+        precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));
     }
 
@@ -472,33 +525,47 @@
         int dy = request.getYIndex() - mouseY;
 
-        return dx * dx + dy * dy;
-    }
-
-    public WMSRequest getRequest() {
+        return 1 + dx * dx + dy * dy;
+    }
+
+    private void sortRequests(boolean localOnly) {
+        Iterator<WMSRequest> it = requestQueue.iterator();
+        while (it.hasNext()) {
+            WMSRequest item = it.next();
+
+            if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
+                it.remove();
+                continue;
+            }
+
+            int priority = getRequestPriority(item);
+            if (priority == -1 && item.isPrecacheOnly()) {
+                priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
+            }
+
+            if (localOnly && !item.hasExactMatch()) {
+                priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
+            }
+
+            if (       priority == -1
+                    || finishedRequests.contains(item)
+                    || processingRequests.contains(item)) {
+                it.remove();
+            } else {
+                item.setPriority(priority);
+            }
+        }
+        Collections.sort(requestQueue);
+    }
+
+    public WMSRequest getRequest(boolean localOnly) {
         requestQueueLock.lock();
         try {
             workingThreadCount--;
-            Iterator<WMSRequest> it = requestQueue.iterator();
-            while (it.hasNext()) {
-                WMSRequest item = it.next();
-                int priority = getRequestPriority(item);
-                if (priority == -1 || finishedRequests.contains(item) || processingRequests.contains(item)) {
-                    it.remove();
-                } else {
-                    item.setPriority(priority);
-                }
-            }
-            Collections.sort(requestQueue);
-
-            EastNorth cursorEastNorth = mv.getEastNorth(mv.lastMEvent.getX(), mv.lastMEvent.getY());
-            int mouseX = getImageXIndex(cursorEastNorth.east());
-            int mouseY = getImageYIndex(cursorEastNorth.north());
-            boolean isOnMouse = requestQueue.size() > 0 && requestQueue.get(0).getXIndex() == mouseX && requestQueue.get(0).getYIndex() == mouseY;
-
-            // If there is only one thread left then keep it in case we need to download other tile urgently
-            while (!canceled &&
-                    (requestQueue.isEmpty() || (!isOnMouse && threadCount - workingThreadCount == 0 && threadCount > 1))) {
+
+            sortRequests(localOnly);
+            while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
                 try {
                     queueEmpty.await();
+                    sortRequests(localOnly);
                 } catch (InterruptedException e) {
                     // Shouldn't happen
@@ -523,6 +590,14 @@
         requestQueueLock.lock();
         try {
+            PrecacheTask task = request.getPrecacheTask();
+            if (task != null) {
+                task.processedCount++;
+                if (!task.progressMonitor.isCanceled()) {
+                    task.progressMonitor.worked(1);
+                    task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
+                }
+            }
             processingRequests.remove(request);
-            if (request.getState() != null) {
+            if (request.getState() != null && !request.isPrecacheOnly()) {
                 finishedRequests.add(request);
                 mv.repaint();
@@ -536,6 +611,16 @@
         requestQueueLock.lock();
         try {
+
+            ProjectionBounds b = getBounds(request);
+            // Checking for exact match is fast enough, no need to do it in separated thread
+            request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
+            if (request.isPrecacheOnly() && request.hasExactMatch())
+                return; // We already have this tile cached
+
             if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
                 requestQueue.add(request);
+                if (request.getPrecacheTask() != null) {
+                    request.getPrecacheTask().totalCount++;
+                }
                 queueEmpty.signalAll();
             }
@@ -545,5 +630,5 @@
     }
 
-    public boolean requestIsValid(WMSRequest request) {
+    public boolean requestIsVisible(WMSRequest request) {
         return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
     }
@@ -871,5 +956,5 @@
             grabberThreads.clear();
             for (int i=0; i<threadCount; i++) {
-                Grabber grabber = getGrabber();
+                Grabber grabber = getGrabber(i == 0 && threadCount > 1);
                 grabbers.add(grabber);
                 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
@@ -914,10 +999,29 @@
     }
 
-    protected Grabber getGrabber(){
+    protected Grabber getGrabber(boolean localOnly){
         if(getInfo().getImageryType() == ImageryType.HTML)
-            return new HTMLGrabber(mv, this);
+            return new HTMLGrabber(mv, this, localOnly);
         else if(getInfo().getImageryType() == ImageryType.WMS)
-            return new WMSGrabber(mv, this);
+            return new WMSGrabber(mv, this, localOnly);
         else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
+    }
+
+    public ProjectionBounds getBounds(WMSRequest request) {
+        ProjectionBounds result = new ProjectionBounds(
+                getEastNorth(request.getXIndex(), request.getYIndex()),
+                getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
+
+        if (WMSLayer.PROP_OVERLAP.get()) {
+            double eastSize =  result.maxEast - result.minEast;
+            double northSize =  result.maxNorth - result.minNorth;
+
+            double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
+            double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
+
+            result = new ProjectionBounds(result.getMin(),
+                    new EastNorth(result.maxEast + eastCoef * eastSize,
+                            result.maxNorth + northCoef * northSize));
+        }
+        return result;
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/progress/ProgressTaskIds.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/progress/ProgressTaskIds.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/gui/progress/ProgressTaskIds.java	(revision 4745)
@@ -5,4 +5,5 @@
 
     ProgressTaskId DOWNLOAD_GPS = new ProgressTaskId("core", "downloadGps");
+    ProgressTaskId PRECACHE_WMS = new ProgressTaskId("core", "precacheWms");
 
 }
Index: trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/io/imagery/Grabber.java	(revision 4745)
@@ -4,7 +4,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.ProjectionBounds;
-import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
-import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.WMSLayer;
@@ -13,35 +11,13 @@
     protected final MapView mv;
     protected final WMSLayer layer;
+    private final boolean localOnly;
 
     protected ProjectionBounds b;
-    protected Projection proj;
-    protected double pixelPerDegree;
-    protected WMSRequest request;
     protected volatile boolean canceled;
 
-    Grabber(MapView mv, WMSLayer layer) {
+    Grabber(MapView mv, WMSLayer layer, boolean localOnly) {
         this.mv = mv;
         this.layer = layer;
-    }
-
-    private void updateState(WMSRequest request) {
-        b = new ProjectionBounds(
-                layer.getEastNorth(request.getXIndex(), request.getYIndex()),
-                layer.getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
-        if (WMSLayer.PROP_OVERLAP.get()) {
-            double eastSize =  b.maxEast - b.minEast;
-            double northSize =  b.maxNorth - b.minNorth;
-
-            double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
-            double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
-
-            this.b = new ProjectionBounds(b.getMin(),
-                    new EastNorth(b.maxEast + eastCoef * eastSize,
-                            b.maxNorth + northCoef * northSize));
-        }
-
-        this.proj = Main.getProjection();
-        this.pixelPerDegree = request.getPixelPerDegree();
-        this.request = request;
+        this.localOnly = localOnly;
     }
 
@@ -60,10 +36,16 @@
             if (canceled)
                 return;
-            WMSRequest request = layer.getRequest();
+            WMSRequest request = layer.getRequest(localOnly);
             if (request == null)
                 return;
-            updateState(request);
-            if(!loadFromCache(request)){
-                attempt(request);
+            this.b = layer.getBounds(request);
+            if (request.isPrecacheOnly()) {
+                if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {
+                    attempt(request);
+                }
+            } else {
+                if(!loadFromCache(request)){
+                    attempt(request);
+                }
             }
             layer.finishRequest(request);
@@ -77,5 +59,5 @@
                 return;
             try {
-                if (!layer.requestIsValid(request))
+                if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))
                     return;
                 fetch(request, i);
Index: trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java	(revision 4745)
@@ -22,10 +22,10 @@
     public static final StringProperty PROP_BROWSER = new StringProperty("imagery.wms.browser", "webkit-image {0}");
 
-    public HTMLGrabber(MapView mv, WMSLayer layer) {
-        super(mv, layer);
+    public HTMLGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
+        super(mv, layer, localOnly);
     }
 
     @Override
-    protected BufferedImage grab(URL url, int attempt) throws IOException {
+    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException {
         String urlstring = url.toExternalForm();
 
@@ -52,5 +52,5 @@
         BufferedImage img = layer.normalizeImage(ImageIO.read(bais));
         bais.reset();
-        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), pixelPerDegree, b.minEast, b.minNorth);
+        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
 
         return img;
Index: trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java	(revision 4745)
@@ -1,6 +1,4 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.io.imagery;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.image.BufferedImage;
@@ -18,15 +16,12 @@
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map.Entry;
+import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
-import java.util.HashMap;
+import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.imageio.ImageIO;
-import javax.swing.JOptionPane;
 
 import org.openstreetmap.josm.Main;
@@ -50,6 +45,6 @@
     private Map<String, String> props = new HashMap<String, String>();
 
-    public WMSGrabber(MapView mv, WMSLayer layer) {
-        super(mv, layer);
+    public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {
+        super(mv, layer, localOnly);
         this.info = layer.getInfo();
         this.baseURL = info.getUrl();
@@ -77,5 +72,5 @@
                     b.maxEast, b.maxNorth,
                     width(), height());
-            request.finish(State.IMAGE, grab(url, attempt));
+            request.finish(State.IMAGE, grab(request, url, attempt));
 
         } catch(Exception e) {
@@ -102,20 +97,20 @@
 
         return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj)
-            .replaceAll("\\{bbox\\}", latLonFormat.format(w) + ","
-                + latLonFormat.format(s) + ","
-                + latLonFormat.format(e) + ","
-                + latLonFormat.format(n))
-            .replaceAll("\\{w\\}", latLonFormat.format(w))
-            .replaceAll("\\{s\\}", latLonFormat.format(s))
-            .replaceAll("\\{e\\}", latLonFormat.format(e))
-            .replaceAll("\\{n\\}", latLonFormat.format(n))
-            .replaceAll("\\{width\\}", String.valueOf(wi))
-            .replaceAll("\\{height\\}", String.valueOf(ht))
-            .replace(" ", "%20"));
+                .replaceAll("\\{bbox\\}", latLonFormat.format(w) + ","
+                        + latLonFormat.format(s) + ","
+                        + latLonFormat.format(e) + ","
+                        + latLonFormat.format(n))
+                        .replaceAll("\\{w\\}", latLonFormat.format(w))
+                        .replaceAll("\\{s\\}", latLonFormat.format(s))
+                        .replaceAll("\\{e\\}", latLonFormat.format(e))
+                        .replaceAll("\\{n\\}", latLonFormat.format(n))
+                        .replaceAll("\\{width\\}", String.valueOf(wi))
+                        .replaceAll("\\{height\\}", String.valueOf(ht))
+                        .replace(" ", "%20"));
     }
 
     @Override
     public boolean loadFromCache(WMSRequest request) {
-        BufferedImage cached = layer.cache.getExactMatch(Main.getProjection(), pixelPerDegree, b.minEast, b.minNorth);
+        BufferedImage cached = layer.cache.getExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
 
         if (cached != null) {
@@ -123,5 +118,5 @@
             return true;
         } else if (request.isAllowPartialCacheMatch()) {
-            BufferedImage partialMatch = layer.cache.getPartialMatch(Main.getProjection(), pixelPerDegree, b.minEast, b.minNorth);
+            BufferedImage partialMatch = layer.cache.getPartialMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
             if (partialMatch != null) {
                 request.finish(State.PARTLY_IN_CACHE, partialMatch);
@@ -138,5 +133,5 @@
     }
 
-    protected BufferedImage grab(URL url, int attempt) throws IOException, OsmTransferException {
+    protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException, OsmTransferException {
         System.out.println("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);
 
@@ -164,5 +159,5 @@
         BufferedImage img = layer.normalizeImage(ImageIO.read(bais));
         bais.reset();
-        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), pixelPerDegree, b.minEast, b.minNorth);
+        layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);
         return img;
     }
Index: trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 4744)
+++ trunk/src/org/openstreetmap/josm/io/imagery/WMSRequest.java	(revision 4745)
@@ -5,4 +5,5 @@
 
 import org.openstreetmap.josm.data.imagery.GeorefImage.State;
+import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
 
 public class WMSRequest implements Comparable<WMSRequest> {
@@ -11,6 +12,8 @@
     private final double pixelPerDegree;
     private final boolean real; // Download even if autodownloading is disabled
+    private final PrecacheTask precacheTask; // Download even when wms tile is not currently visible (precache)
     private final boolean allowPartialCacheMatch;
     private int priority;
+    private boolean hasExactMatch;
     // Result
     private State state;
@@ -18,10 +21,16 @@
 
     public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch) {
+        this(xIndex, yIndex, pixelPerDegree, real, allowPartialCacheMatch, null);
+    }
+
+    public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch, PrecacheTask precacheTask) {
         this.xIndex = xIndex;
         this.yIndex = yIndex;
         this.pixelPerDegree = pixelPerDegree;
         this.real = real;
+        this.precacheTask = precacheTask;
         this.allowPartialCacheMatch = allowPartialCacheMatch;
     }
+
 
     public void finish(State state, BufferedImage image) {
@@ -99,5 +108,5 @@
     public String toString() {
         return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex
-        + ", pixelPerDegree=" + pixelPerDegree + "]";
+                + ", pixelPerDegree=" + pixelPerDegree + "]";
     }
 
@@ -106,6 +115,22 @@
     }
 
+    public boolean isPrecacheOnly() {
+        return precacheTask != null;
+    }
+
+    public PrecacheTask getPrecacheTask() {
+        return precacheTask;
+    }
+
     public boolean isAllowPartialCacheMatch() {
         return allowPartialCacheMatch;
     }
+
+    public boolean hasExactMatch() {
+        return hasExactMatch;
+    }
+
+    public void setHasExactMatch(boolean hasExactMatch) {
+        this.hasExactMatch = hasExactMatch;
+    }
 }
