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