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