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