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 if (Main.map != null && Main.map.mapView != null) { 603 Main.map.mapView.repaint(); 604 } 605 } 606 } finally { 607 requestQueueLock.unlock(); 608 } 609 } 610 611 public void addRequest(WMSRequest request) { 612 requestQueueLock.lock(); 613 try { 614 615 if (cache != null) { 616 ProjectionBounds b = getBounds(request); 617 // Checking for exact match is fast enough, no need to do it in separated thread 618 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)); 619 if (request.isPrecacheOnly() && request.hasExactMatch()) 620 return; // We already have this tile cached 621 } 622 623 if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) { 624 requestQueue.add(request); 625 if (request.getPrecacheTask() != null) { 626 request.getPrecacheTask().totalCount++; 627 } 628 queueEmpty.signalAll(); 629 } 630 } finally { 631 requestQueueLock.unlock(); 632 } 633 } 634 635 public boolean requestIsVisible(WMSRequest request) { 636 return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex(); 637 } 638 639 private void gatherFinishedRequests() { 640 requestQueueLock.lock(); 641 try { 642 for (WMSRequest request: finishedRequests) { 643 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)]; 644 if (img.equalPosition(request.getXIndex(), request.getYIndex())) { 645 img.changeImage(request.getState(), request.getImage()); 646 } 647 } 648 } finally { 649 requestQueueLock.unlock(); 650 finishedRequests.clear(); 651 } 652 } 653 654 public class DownloadAction extends AbstractAction { 655 public DownloadAction() { 656 super(tr("Download visible tiles")); 657 } 658 @Override 659 public void actionPerformed(ActionEvent ev) { 660 if (zoomIsTooBig()) { 661 JOptionPane.showMessageDialog( 662 Main.parent, 663 tr("The requested area is too big. Please zoom in a little, or change resolution"), 664 tr("Error"), 665 JOptionPane.ERROR_MESSAGE 666 ); 667 } else { 668 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true); 669 } 670 } 671 } 672 673 public static class ChangeResolutionAction extends AbstractAction implements LayerAction { 674 public ChangeResolutionAction() { 675 super(tr("Change resolution")); 676 } 677 678 private void changeResolution(WMSLayer layer) { 679 layer.resolution = Main.map.mapView.getDist100PixelText(); 680 layer.info.setPixelPerDegree(layer.getPPD()); 681 layer.settingsChanged = true; 682 for(int x = 0; x<layer.dax; ++x) { 683 for(int y = 0; y<layer.day; ++y) { 684 layer.images[x][y].changePosition(-1, -1); 685 } 686 } 687 } 688 689 @Override 690 public void actionPerformed(ActionEvent ev) { 691 692 if (LayerListDialog.getInstance() == null) 693 return; 694 695 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 696 for (Layer l: layers) { 697 changeResolution((WMSLayer) l); 698 } 699 Main.map.mapView.repaint(); 700 } 701 702 @Override 703 public boolean supportLayers(List<Layer> layers) { 704 for (Layer l: layers) { 705 if (!(l instanceof WMSLayer)) 706 return false; 707 } 708 return true; 709 } 710 711 @Override 712 public Component createMenuComponent() { 713 return new JMenuItem(this); 714 } 715 716 @Override 717 public boolean equals(Object obj) { 718 return obj instanceof ChangeResolutionAction; 719 } 720 } 721 722 public class ReloadErrorTilesAction extends AbstractAction { 723 public ReloadErrorTilesAction() { 724 super(tr("Reload erroneous tiles")); 725 } 726 @Override 727 public void actionPerformed(ActionEvent ev) { 728 // Delete small files, because they're probably blank tiles. 729 // See https://josm.openstreetmap.de/ticket/2307 730 cache.cleanSmallFiles(4096); 731 732 for (int x = 0; x < dax; ++x) { 733 for (int y = 0; y < day; ++y) { 734 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 735 if(img.getState() == State.FAILED){ 736 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false)); 737 } 738 } 739 } 740 } 741 } 742 743 public class ToggleAlphaAction extends AbstractAction implements LayerAction { 744 public ToggleAlphaAction() { 745 super(tr("Alpha channel")); 746 } 747 @Override 748 public void actionPerformed(ActionEvent ev) { 749 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 750 boolean alphaChannel = checkbox.isSelected(); 751 PROP_ALPHA_CHANNEL.put(alphaChannel); 752 753 // clear all resized cached instances and repaint the layer 754 for (int x = 0; x < dax; ++x) { 755 for (int y = 0; y < day; ++y) { 756 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 757 img.flushedResizedCachedInstance(); 758 } 759 } 760 Main.map.mapView.repaint(); 761 } 762 @Override 763 public Component createMenuComponent() { 764 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 765 item.setSelected(PROP_ALPHA_CHANNEL.get()); 766 return item; 767 } 768 @Override 769 public boolean supportLayers(List<Layer> layers) { 770 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 771 } 772 } 773 774 /** 775 * This action will add a WMS layer menu entry with the current WMS layer 776 * URL and name extended by the current resolution. 777 * When using the menu entry again, the WMS cache will be used properly. 778 */ 779 public class BookmarkWmsAction extends AbstractAction { 780 public BookmarkWmsAction() { 781 super(tr("Set WMS Bookmark")); 782 } 783 @Override 784 public void actionPerformed(ActionEvent ev) { 785 ImageryLayerInfo.addLayer(new ImageryInfo(info)); 786 } 787 } 788 789 private class StartStopAction extends AbstractAction implements LayerAction { 790 791 public StartStopAction() { 792 super(tr("Automatic downloading")); 793 } 794 795 @Override 796 public Component createMenuComponent() { 797 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 798 item.setSelected(autoDownloadEnabled); 799 return item; 800 } 801 802 @Override 803 public boolean supportLayers(List<Layer> layers) { 804 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 805 } 806 807 @Override 808 public void actionPerformed(ActionEvent e) { 809 autoDownloadEnabled = !autoDownloadEnabled; 810 if (autoDownloadEnabled) { 811 for (int x = 0; x < dax; ++x) { 812 for (int y = 0; y < day; ++y) { 813 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 814 if(img.getState() == State.NOT_IN_CACHE){ 815 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true)); 816 } 817 } 818 } 819 Main.map.mapView.repaint(); 820 } 821 } 822 } 823 824 private class ZoomToNativeResolution extends AbstractAction { 825 826 public ZoomToNativeResolution() { 827 super(tr("Zoom to native resolution")); 828 } 829 830 @Override 831 public void actionPerformed(ActionEvent e) { 832 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree()); 833 } 834 835 } 836 837 private void cancelGrabberThreads(boolean wait) { 838 requestQueueLock.lock(); 839 try { 840 canceled = true; 841 for (Grabber grabber: grabbers) { 842 grabber.cancel(); 843 } 844 queueEmpty.signalAll(); 845 } finally { 846 requestQueueLock.unlock(); 847 } 848 if (wait) { 849 for (Thread t: grabberThreads) { 850 try { 851 t.join(); 852 } catch (InterruptedException e) { 853 // Shouldn't happen 854 e.printStackTrace(); 855 } 856 } 857 } 858 } 859 860 private void startGrabberThreads() { 861 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get(); 862 requestQueueLock.lock(); 863 try { 864 canceled = false; 865 grabbers.clear(); 866 grabberThreads.clear(); 867 for (int i=0; i<threadCount; i++) { 868 Grabber grabber = getGrabber(i == 0 && threadCount > 1); 869 grabbers.add(grabber); 870 Thread t = new Thread(grabber, "WMS " + getName() + " " + i); 871 t.setDaemon(true); 872 t.start(); 873 grabberThreads.add(t); 874 } 875 this.workingThreadCount = grabbers.size(); 876 this.threadCount = grabbers.size(); 877 } finally { 878 requestQueueLock.unlock(); 879 } 880 } 881 882 @Override 883 public boolean isChanged() { 884 requestQueueLock.lock(); 885 try { 886 return !finishedRequests.isEmpty() || settingsChanged; 887 } finally { 888 requestQueueLock.unlock(); 889 } 890 } 891 892 @Override 893 public void preferenceChanged(PreferenceChangeEvent event) { 894 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) { 895 cancelGrabberThreads(true); 896 startGrabberThreads(); 897 } else if ( 898 event.getKey().equals(PROP_OVERLAP.getKey()) 899 || event.getKey().equals(PROP_OVERLAP_EAST.getKey()) 900 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) { 901 for (int i=0; i<images.length; i++) { 902 for (int k=0; k<images[i].length; k++) { 903 images[i][k] = new GeorefImage(this); 904 } 905 } 906 907 settingsChanged = true; 908 } 909 } 910 911 protected Grabber getGrabber(boolean localOnly) { 912 if (getInfo().getImageryType() == ImageryType.HTML) 913 return new HTMLGrabber(Main.map.mapView, this, localOnly); 914 else if (getInfo().getImageryType() == ImageryType.WMS) 915 return new WMSGrabber(Main.map.mapView, this, localOnly); 916 else throw new IllegalStateException("getGrabber() called for non-WMS layer type"); 917 } 918 919 public ProjectionBounds getBounds(WMSRequest request) { 920 ProjectionBounds result = new ProjectionBounds( 921 getEastNorth(request.getXIndex(), request.getYIndex()), 922 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1)); 923 924 if (WMSLayer.PROP_OVERLAP.get()) { 925 double eastSize = result.maxEast - result.minEast; 926 double northSize = result.maxNorth - result.minNorth; 927 928 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0; 929 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0; 930 931 result = new ProjectionBounds(result.getMin(), 932 new EastNorth(result.maxEast + eastCoef * eastSize, 933 result.maxNorth + northCoef * northSize)); 934 } 935 return result; 936 } 937 938 @Override 939 public boolean isProjectionSupported(Projection proj) { 940 List<String> serverProjections = info.getServerProjections(); 941 return serverProjections.contains(proj.toCode().toUpperCase()) 942 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84"))) 943 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84")); 944 } 945 946 @Override 947 public String nameSupportedProjections() { 948 String res = ""; 949 for(String p : info.getServerProjections()) { 950 if(!res.isEmpty()) { 951 res += ", "; 952 } 953 res += p; 954 } 955 return tr("Supported projections are: {0}", res); 956 } 957 958 @Override 959 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 960 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0); 961 Main.map.repaint(done ? 0 : 100); 962 return !done; 963 } 964 965 @Override 966 public void writeExternal(ObjectOutput out) throws IOException { 967 out.writeInt(serializeFormatVersion); 968 out.writeInt(dax); 969 out.writeInt(day); 970 out.writeInt(imageSize); 971 out.writeDouble(info.getPixelPerDegree()); 972 out.writeObject(info.getName()); 973 out.writeObject(info.getExtendedUrl()); 974 out.writeObject(images); 975 } 976 977 @Override 978 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 979 int sfv = in.readInt(); 980 if (sfv != serializeFormatVersion) 981 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion)); 982 autoDownloadEnabled = false; 983 dax = in.readInt(); 984 day = in.readInt(); 985 imageSize = in.readInt(); 986 info.setPixelPerDegree(in.readDouble()); 987 doSetName((String)in.readObject()); 988 info.setExtendedUrl((String)in.readObject()); 989 images = (GeorefImage[][])in.readObject(); 990 991 for (GeorefImage[] imgs : images) { 992 for (GeorefImage img : imgs) { 993 if (img != null) { 994 img.setLayer(WMSLayer.this); 995 } 996 } 997 } 998 999 settingsChanged = true; 1000 if (Main.isDisplayingMapView()) { 1001 Main.map.mapView.repaint(); 1002 } 1003 if (cache != null) { 1004 cache.saveIndex(); 1005 cache = null; 1006 } 1007 } 1008 1009 @Override 1010 public void onPostLoadFromFile() { 1011 if (info.getUrl() != null) { 1012 cache = new WmsCache(info.getUrl(), imageSize); 1013 startGrabberThreads(); 1014 } 1015 } 1016 1017 @Override 1018 public boolean isSavable() { 1019 return true; // With WMSLayerExporter 1020 } 1021 1022 @Override 1023 public File createAndOpenSaveFileChooser() { 1024 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1025 } 1026 }