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