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; 068 069 070/** 071 * This is a layer that grabs the current screen from an WMS server. The data 072 * fetched this way is tiled and managed to the disc to reduce server load. 073 */ 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<WMSRequest>(); 142 private final List<WMSRequest> finishedRequests = new ArrayList<WMSRequest>(); 143 /** 144 * List of request currently being processed by download threads 145 */ 146 private final List<WMSRequest> processingRequests = new ArrayList<WMSRequest>(); 147 private final Lock requestQueueLock = new ReentrantLock(); 148 private final Condition queueEmpty = requestQueueLock.newCondition(); 149 private final List<Grabber> grabbers = new ArrayList<Grabber>(); 150 private final List<Thread> grabberThreads = new ArrayList<Thread>(); 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 public WMSLayer() { 159 this(new ImageryInfo(tr("Blank Layer"))); 160 } 161 162 public WMSLayer(ImageryInfo info) { 163 super(info); 164 imageSize = PROP_IMAGE_SIZE.get(); 165 setBackgroundLayer(true); /* set global background variable */ 166 initializeImages(); 167 168 attribution.initialize(this.info); 169 170 Main.pref.addPreferenceChangeListener(this); 171 } 172 173 @Override 174 public void hookUpMapView() { 175 if (info.getUrl() != null) { 176 startGrabberThreads(); 177 178 for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) { 179 if (layer.getInfo().getUrl().equals(info.getUrl())) { 180 cache = layer.cache; 181 break; 182 } 183 } 184 if (cache == null) { 185 cache = new WmsCache(info.getUrl(), imageSize); 186 cache.loadIndex(); 187 } 188 } 189 190 // if automatic resolution is enabled, ensure that the first zoom level 191 // is already snapped. Otherwise it may load tiles that will never get 192 // used again when zooming. 193 updateResolutionSetting(this, autoResolutionEnabled); 194 195 final MouseAdapter adapter = new MouseAdapter() { 196 @Override 197 public void mouseClicked(MouseEvent e) { 198 if (!isVisible()) return; 199 if (e.getButton() == MouseEvent.BUTTON1) { 200 attribution.handleAttribution(e.getPoint(), true); 201 } 202 } 203 }; 204 Main.map.mapView.addMouseListener(adapter); 205 206 MapView.addLayerChangeListener(new LayerChangeListener() { 207 @Override 208 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 209 // 210 } 211 212 @Override 213 public void layerAdded(Layer newLayer) { 214 // 215 } 216 217 @Override 218 public void layerRemoved(Layer oldLayer) { 219 if (oldLayer == WMSLayer.this) { 220 Main.map.mapView.removeMouseListener(adapter); 221 MapView.removeLayerChangeListener(this); 222 } 223 } 224 }); 225 } 226 227 public void doSetName(String name) { 228 setName(name); 229 info.setName(name); 230 } 231 232 public boolean hasAutoDownload(){ 233 return autoDownloadEnabled; 234 } 235 236 public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) { 237 Set<Point> requestedTiles = new HashSet<Point>(); 238 for (LatLon point: points) { 239 EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX)); 240 EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX)); 241 int minX = getImageXIndex(minEn.east()); 242 int maxX = getImageXIndex(maxEn.east()); 243 int minY = getImageYIndex(minEn.north()); 244 int maxY = getImageYIndex(maxEn.north()); 245 246 for (int x=minX; x<=maxX; x++) { 247 for (int y=minY; y<=maxY; y++) { 248 requestedTiles.add(new Point(x, y)); 249 } 250 } 251 } 252 253 for (Point p: requestedTiles) { 254 addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask)); 255 } 256 257 precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount()); 258 precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount)); 259 } 260 261 @Override 262 public void destroy() { 263 super.destroy(); 264 cancelGrabberThreads(false); 265 Main.pref.removePreferenceChangeListener(this); 266 if (cache != null) { 267 cache.saveIndex(); 268 } 269 } 270 271 public void initializeImages() { 272 GeorefImage[][] old = images; 273 images = new GeorefImage[dax][day]; 274 if (old != null) { 275 for (GeorefImage[] row : old) { 276 for (GeorefImage image : row) { 277 images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image; 278 } 279 } 280 } 281 for(int x = 0; x<dax; ++x) { 282 for(int y = 0; y<day; ++y) { 283 if (images[x][y] == null) { 284 images[x][y]= new GeorefImage(this); 285 } 286 } 287 } 288 } 289 290 @Override public ImageryInfo getInfo() { 291 return info; 292 } 293 294 @Override public String getToolTipText() { 295 if(autoDownloadEnabled) 296 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText); 297 else 298 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText); 299 } 300 301 private int modulo (int a, int b) { 302 return a % b >= 0 ? a%b : a%b+b; 303 } 304 305 private boolean zoomIsTooBig() { 306 //don't download when it's too outzoomed 307 return info.getPixelPerDegree() / getPPD() > minZoom; 308 } 309 310 @Override public void paint(Graphics2D g, final MapView mv, Bounds b) { 311 if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return; 312 313 if (autoResolutionEnabled && getBestZoom() != mv.getDist100Pixel()) { 314 changeResolution(this, true); 315 } 316 317 settingsChanged = false; 318 319 ProjectionBounds bounds = mv.getProjectionBounds(); 320 bminx= getImageXIndex(bounds.minEast); 321 bminy= getImageYIndex(bounds.minNorth); 322 bmaxx= getImageXIndex(bounds.maxEast); 323 bmaxy= getImageYIndex(bounds.maxNorth); 324 325 leftEdge = (int)(bounds.minEast * getPPD()); 326 bottomEdge = (int)(bounds.minNorth * getPPD()); 327 328 if (zoomIsTooBig()) { 329 for(int x = 0; x<images.length; ++x) { 330 for(int y = 0; y<images[0].length; ++y) { 331 GeorefImage image = images[x][y]; 332 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge); 333 } 334 } 335 } else { 336 downloadAndPaintVisible(g, mv, false); 337 } 338 339 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this); 340 341 } 342 343 @Override 344 public void setOffset(double dx, double dy) { 345 super.setOffset(dx, dy); 346 settingsChanged = true; 347 } 348 349 public int getImageXIndex(double coord) { 350 return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize); 351 } 352 353 public int getImageYIndex(double coord) { 354 return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize); 355 } 356 357 public int getImageX(int imageIndex) { 358 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD()); 359 } 360 361 public int getImageY(int imageIndex) { 362 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD()); 363 } 364 365 public int getImageWidth(int xIndex) { 366 return getImageX(xIndex + 1) - getImageX(xIndex); 367 } 368 369 public int getImageHeight(int yIndex) { 370 return getImageY(yIndex + 1) - getImageY(yIndex); 371 } 372 373 /** 374 * 375 * @return Size of image in original zoom 376 */ 377 public int getBaseImageWidth() { 378 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_EAST.get() * imageSize / 100) : 0; 379 return imageSize + overlap; 380 } 381 382 /** 383 * 384 * @return Size of image in original zoom 385 */ 386 public int getBaseImageHeight() { 387 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_NORTH.get() * imageSize / 100) : 0; 388 return imageSize + overlap; 389 } 390 391 public int getImageSize() { 392 return imageSize; 393 } 394 395 public boolean isOverlapEnabled() { 396 return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0); 397 } 398 399 /** 400 * 401 * @return When overlapping is enabled, return visible part of tile. Otherwise return original image 402 */ 403 public BufferedImage normalizeImage(BufferedImage img) { 404 if (isOverlapEnabled()) { 405 BufferedImage copy = img; 406 img = new BufferedImage(imageSize, imageSize, copy.getType()); 407 img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize, 408 0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null); 409 } 410 return img; 411 } 412 413 /** 414 * 415 * @param xIndex 416 * @param yIndex 417 * @return Real EastNorth of given tile. dx/dy is not counted in 418 */ 419 public EastNorth getEastNorth(int xIndex, int yIndex) { 420 return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree()); 421 } 422 423 protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){ 424 425 int newDax = dax; 426 int newDay = day; 427 428 if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) { 429 newDax = ((bmaxx - bminx) / daStep + 1) * daStep; 430 } 431 432 if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) { 433 newDay = ((bmaxy - bminy) / daStep + 1) * daStep; 434 } 435 436 if (newDax != dax || newDay != day) { 437 dax = newDax; 438 day = newDay; 439 initializeImages(); 440 } 441 442 for(int x = bminx; x<=bmaxx; ++x) { 443 for(int y = bminy; y<=bmaxy; ++y){ 444 images[modulo(x,dax)][modulo(y,day)].changePosition(x, y); 445 } 446 } 447 448 gatherFinishedRequests(); 449 Set<ProjectionBounds> areaToCache = new HashSet<ProjectionBounds>(); 450 451 for(int x = bminx; x<=bmaxx; ++x) { 452 for(int y = bminy; y<=bmaxy; ++y){ 453 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 454 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) { 455 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true); 456 addRequest(request); 457 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 458 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) { 459 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false); 460 addRequest(request); 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 public DownloadAction() { 661 super(tr("Download visible tiles")); 662 } 663 @Override 664 public void actionPerformed(ActionEvent ev) { 665 if (zoomIsTooBig()) { 666 JOptionPane.showMessageDialog( 667 Main.parent, 668 tr("The requested area is too big. Please zoom in a little, or change resolution"), 669 tr("Error"), 670 JOptionPane.ERROR_MESSAGE 671 ); 672 } else { 673 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true); 674 } 675 } 676 } 677 678 /** 679 * Finds the most suitable resolution for the current zoom level, but prefers 680 * higher resolutions. Snaps to values defined in snapLevels. 681 * @return 682 */ 683 private static double getBestZoom() { 684 // not sure why getDist100Pixel returns values corresponding to 685 // the snapLevels, which are in meters per pixel. It works, though. 686 double dist = Main.map.mapView.getDist100Pixel(); 687 for(int i = snapLevels.length-2; i >= 0; i--) { 688 if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist) 689 return snapLevels[i+1]; 690 } 691 return snapLevels[0]; 692 } 693 694 /** 695 * Updates the given layer’s resolution settings to the current zoom level. Does 696 * not update existing tiles, only new ones will be subject to the new settings. 697 * 698 * @param layer 699 * @param snap Set to true if the resolution should snap to certain values instead of 700 * matching the current zoom level perfectly 701 */ 702 private static void updateResolutionSetting(WMSLayer layer, boolean snap) { 703 if(snap) { 704 layer.resolution = getBestZoom(); 705 layer.resolutionText = MapView.getDistText(layer.resolution); 706 } else { 707 layer.resolution = Main.map.mapView.getDist100Pixel(); 708 layer.resolutionText = Main.map.mapView.getDist100PixelText(); 709 } 710 layer.info.setPixelPerDegree(layer.getPPD()); 711 } 712 713 /** 714 * Updates the given layer’s resolution settings to the current zoom level and 715 * updates existing tiles. If round is true, tiles will be updated gradually, if 716 * false they will be removed instantly (and redrawn only after the new resolution 717 * image has been loaded). 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 changeResolution(WMSLayer layer, boolean snap) { 723 updateResolutionSetting(layer, snap); 724 725 layer.settingsChanged = true; 726 727 // Don’t move tiles off screen when the resolution is rounded. This 728 // prevents some flickering when zooming with auto-resolution enabled 729 // and instead gradually updates each tile. 730 if(!snap) { 731 for(int x = 0; x<layer.dax; ++x) { 732 for(int y = 0; y<layer.day; ++y) { 733 layer.images[x][y].changePosition(-1, -1); 734 } 735 } 736 } 737 } 738 739 public static class ChangeResolutionAction extends AbstractAction implements LayerAction { 740 741 /** 742 * Constructs a new {@code ChangeResolutionAction} 743 */ 744 public ChangeResolutionAction() { 745 super(tr("Change resolution")); 746 } 747 748 @Override 749 public void actionPerformed(ActionEvent ev) { 750 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 751 for (Layer l: layers) { 752 changeResolution((WMSLayer) l, false); 753 } 754 Main.map.mapView.repaint(); 755 } 756 757 @Override 758 public boolean supportLayers(List<Layer> layers) { 759 for (Layer l: layers) { 760 if (!(l instanceof WMSLayer)) 761 return false; 762 } 763 return true; 764 } 765 766 @Override 767 public Component createMenuComponent() { 768 return new JMenuItem(this); 769 } 770 771 @Override 772 public boolean equals(Object obj) { 773 return obj instanceof ChangeResolutionAction; 774 } 775 } 776 777 public class ReloadErrorTilesAction extends AbstractAction { 778 public ReloadErrorTilesAction() { 779 super(tr("Reload erroneous tiles")); 780 } 781 @Override 782 public void actionPerformed(ActionEvent ev) { 783 // Delete small files, because they're probably blank tiles. 784 // See #2307 785 cache.cleanSmallFiles(4096); 786 787 for (int x = 0; x < dax; ++x) { 788 for (int y = 0; y < day; ++y) { 789 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 790 if(img.getState() == State.FAILED){ 791 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false)); 792 } 793 } 794 } 795 } 796 } 797 798 public class ToggleAlphaAction extends AbstractAction implements LayerAction { 799 public ToggleAlphaAction() { 800 super(tr("Alpha channel")); 801 } 802 @Override 803 public void actionPerformed(ActionEvent ev) { 804 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 805 boolean alphaChannel = checkbox.isSelected(); 806 PROP_ALPHA_CHANNEL.put(alphaChannel); 807 808 // clear all resized cached instances and repaint the layer 809 for (int x = 0; x < dax; ++x) { 810 for (int y = 0; y < day; ++y) { 811 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 812 img.flushedResizedCachedInstance(); 813 } 814 } 815 Main.map.mapView.repaint(); 816 } 817 @Override 818 public Component createMenuComponent() { 819 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 820 item.setSelected(PROP_ALPHA_CHANNEL.get()); 821 return item; 822 } 823 @Override 824 public boolean supportLayers(List<Layer> layers) { 825 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 826 } 827 } 828 829 830 public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction { 831 public ToggleAutoResolutionAction() { 832 super(tr("Automatically change resolution")); 833 } 834 835 @Override 836 public void actionPerformed(ActionEvent ev) { 837 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 838 autoResolutionEnabled = checkbox.isSelected(); 839 } 840 841 @Override 842 public Component createMenuComponent() { 843 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 844 item.setSelected(autoResolutionEnabled); 845 return item; 846 } 847 848 @Override 849 public boolean supportLayers(List<Layer> layers) { 850 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 851 } 852 } 853 854 /** 855 * This action will add a WMS layer menu entry with the current WMS layer 856 * URL and name extended by the current resolution. 857 * When using the menu entry again, the WMS cache will be used properly. 858 */ 859 public class BookmarkWmsAction extends AbstractAction { 860 public BookmarkWmsAction() { 861 super(tr("Set WMS Bookmark")); 862 } 863 @Override 864 public void actionPerformed(ActionEvent ev) { 865 ImageryLayerInfo.addLayer(new ImageryInfo(info)); 866 } 867 } 868 869 private class StartStopAction extends AbstractAction implements LayerAction { 870 871 public StartStopAction() { 872 super(tr("Automatic downloading")); 873 } 874 875 @Override 876 public Component createMenuComponent() { 877 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 878 item.setSelected(autoDownloadEnabled); 879 return item; 880 } 881 882 @Override 883 public boolean supportLayers(List<Layer> layers) { 884 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 885 } 886 887 @Override 888 public void actionPerformed(ActionEvent e) { 889 autoDownloadEnabled = !autoDownloadEnabled; 890 if (autoDownloadEnabled) { 891 for (int x = 0; x < dax; ++x) { 892 for (int y = 0; y < day; ++y) { 893 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 894 if(img.getState() == State.NOT_IN_CACHE){ 895 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true)); 896 } 897 } 898 } 899 Main.map.mapView.repaint(); 900 } 901 } 902 } 903 904 private class ZoomToNativeResolution extends AbstractAction { 905 906 public ZoomToNativeResolution() { 907 super(tr("Zoom to native resolution")); 908 } 909 910 @Override 911 public void actionPerformed(ActionEvent e) { 912 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree()); 913 } 914 915 } 916 917 private void cancelGrabberThreads(boolean wait) { 918 requestQueueLock.lock(); 919 try { 920 canceled = true; 921 for (Grabber grabber: grabbers) { 922 grabber.cancel(); 923 } 924 queueEmpty.signalAll(); 925 } finally { 926 requestQueueLock.unlock(); 927 } 928 if (wait) { 929 for (Thread t: grabberThreads) { 930 try { 931 t.join(); 932 } catch (InterruptedException e) { 933 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads"); 934 } 935 } 936 } 937 } 938 939 private void startGrabberThreads() { 940 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get(); 941 requestQueueLock.lock(); 942 try { 943 canceled = false; 944 grabbers.clear(); 945 grabberThreads.clear(); 946 for (int i=0; i<threadCount; i++) { 947 Grabber grabber = getGrabber(i == 0 && threadCount > 1); 948 grabbers.add(grabber); 949 Thread t = new Thread(grabber, "WMS " + getName() + " " + i); 950 t.setDaemon(true); 951 t.start(); 952 grabberThreads.add(t); 953 } 954 } finally { 955 requestQueueLock.unlock(); 956 } 957 } 958 959 @Override 960 public boolean isChanged() { 961 requestQueueLock.lock(); 962 try { 963 return !finishedRequests.isEmpty() || settingsChanged; 964 } finally { 965 requestQueueLock.unlock(); 966 } 967 } 968 969 @Override 970 public void preferenceChanged(PreferenceChangeEvent event) { 971 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) { 972 cancelGrabberThreads(true); 973 startGrabberThreads(); 974 } else if ( 975 event.getKey().equals(PROP_OVERLAP.getKey()) 976 || event.getKey().equals(PROP_OVERLAP_EAST.getKey()) 977 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) { 978 for (int i=0; i<images.length; i++) { 979 for (int k=0; k<images[i].length; k++) { 980 images[i][k] = new GeorefImage(this); 981 } 982 } 983 984 settingsChanged = true; 985 } 986 } 987 988 protected Grabber getGrabber(boolean localOnly) { 989 if (getInfo().getImageryType() == ImageryType.HTML) 990 return new HTMLGrabber(Main.map.mapView, this, localOnly); 991 else if (getInfo().getImageryType() == ImageryType.WMS) 992 return new WMSGrabber(Main.map.mapView, this, localOnly); 993 else throw new IllegalStateException("getGrabber() called for non-WMS layer type"); 994 } 995 996 public ProjectionBounds getBounds(WMSRequest request) { 997 ProjectionBounds result = new ProjectionBounds( 998 getEastNorth(request.getXIndex(), request.getYIndex()), 999 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1)); 1000 1001 if (WMSLayer.PROP_OVERLAP.get()) { 1002 double eastSize = result.maxEast - result.minEast; 1003 double northSize = result.maxNorth - result.minNorth; 1004 1005 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0; 1006 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0; 1007 1008 result = new ProjectionBounds(result.getMin(), 1009 new EastNorth(result.maxEast + eastCoef * eastSize, 1010 result.maxNorth + northCoef * northSize)); 1011 } 1012 return result; 1013 } 1014 1015 @Override 1016 public boolean isProjectionSupported(Projection proj) { 1017 List<String> serverProjections = info.getServerProjections(); 1018 return serverProjections.contains(proj.toCode().toUpperCase()) 1019 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84"))) 1020 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84")); 1021 } 1022 1023 @Override 1024 public String nameSupportedProjections() { 1025 StringBuilder res = new StringBuilder(); 1026 for (String p : info.getServerProjections()) { 1027 if (res.length() > 0) { 1028 res.append(", "); 1029 } 1030 res.append(p); 1031 } 1032 return tr("Supported projections are: {0}", res); 1033 } 1034 1035 @Override 1036 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 1037 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0); 1038 Main.map.repaint(done ? 0 : 100); 1039 return !done; 1040 } 1041 1042 @Override 1043 public void writeExternal(ObjectOutput out) throws IOException { 1044 out.writeInt(serializeFormatVersion); 1045 out.writeInt(dax); 1046 out.writeInt(day); 1047 out.writeInt(imageSize); 1048 out.writeDouble(info.getPixelPerDegree()); 1049 out.writeObject(info.getName()); 1050 out.writeObject(info.getExtendedUrl()); 1051 out.writeObject(images); 1052 } 1053 1054 @Override 1055 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 1056 int sfv = in.readInt(); 1057 if (sfv != serializeFormatVersion) 1058 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion)); 1059 autoDownloadEnabled = false; 1060 dax = in.readInt(); 1061 day = in.readInt(); 1062 imageSize = in.readInt(); 1063 info.setPixelPerDegree(in.readDouble()); 1064 doSetName((String)in.readObject()); 1065 info.setExtendedUrl((String)in.readObject()); 1066 images = (GeorefImage[][])in.readObject(); 1067 1068 for (GeorefImage[] imgs : images) { 1069 for (GeorefImage img : imgs) { 1070 if (img != null) { 1071 img.setLayer(WMSLayer.this); 1072 } 1073 } 1074 } 1075 1076 settingsChanged = true; 1077 if (Main.isDisplayingMapView()) { 1078 Main.map.mapView.repaint(); 1079 } 1080 if (cache != null) { 1081 cache.saveIndex(); 1082 cache = null; 1083 } 1084 } 1085 1086 @Override 1087 public void onPostLoadFromFile() { 1088 if (info.getUrl() != null) { 1089 cache = new WmsCache(info.getUrl(), imageSize); 1090 startGrabberThreads(); 1091 } 1092 } 1093 1094 @Override 1095 public boolean isSavable() { 1096 return true; // With WMSLayerExporter 1097 } 1098 1099 @Override 1100 public File createAndOpenSaveFileChooser() { 1101 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1102 } 1103}