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 cancelGrabberThreads(false); 260 Main.pref.removePreferenceChangeListener(this); 261 if (cache != null) { 262 cache.saveIndex(); 263 } 264 } 265 266 public void initializeImages() { 267 GeorefImage[][] old = images; 268 images = new GeorefImage[dax][day]; 269 if (old != null) { 270 for (int i=0; i<old.length; i++) { 271 for (int k=0; k<old[i].length; k++) { 272 GeorefImage o = old[i][k]; 273 images[modulo(o.getXIndex(),dax)][modulo(o.getYIndex(),day)] = old[i][k]; 274 } 275 } 276 } 277 for(int x = 0; x<dax; ++x) { 278 for(int y = 0; y<day; ++y) { 279 if (images[x][y] == null) { 280 images[x][y]= new GeorefImage(this); 281 } 282 } 283 } 284 } 285 286 @Override public ImageryInfo getInfo() { 287 return info; 288 } 289 290 @Override public String getToolTipText() { 291 if(autoDownloadEnabled) 292 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolution); 293 else 294 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolution); 295 } 296 297 private int modulo (int a, int b) { 298 return a % b >= 0 ? a%b : a%b+b; 299 } 300 301 private boolean zoomIsTooBig() { 302 //don't download when it's too outzoomed 303 return info.getPixelPerDegree() / getPPD() > minZoom; 304 } 305 306 @Override public void paint(Graphics2D g, final MapView mv, Bounds b) { 307 if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return; 308 309 settingsChanged = false; 310 311 ProjectionBounds bounds = mv.getProjectionBounds(); 312 bminx= getImageXIndex(bounds.minEast); 313 bminy= getImageYIndex(bounds.minNorth); 314 bmaxx= getImageXIndex(bounds.maxEast); 315 bmaxy= getImageYIndex(bounds.maxNorth); 316 317 leftEdge = (int)(bounds.minEast * getPPD()); 318 bottomEdge = (int)(bounds.minNorth * getPPD()); 319 320 if (zoomIsTooBig()) { 321 for(int x = 0; x<images.length; ++x) { 322 for(int y = 0; y<images[0].length; ++y) { 323 GeorefImage image = images[x][y]; 324 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge); 325 } 326 } 327 } else { 328 downloadAndPaintVisible(g, mv, false); 329 } 330 331 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this); 332 333 } 334 335 @Override 336 public void setOffset(double dx, double dy) { 337 super.setOffset(dx, dy); 338 settingsChanged = true; 339 } 340 341 public int getImageXIndex(double coord) { 342 return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize); 343 } 344 345 public int getImageYIndex(double coord) { 346 return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize); 347 } 348 349 public int getImageX(int imageIndex) { 350 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD()); 351 } 352 353 public int getImageY(int imageIndex) { 354 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD()); 355 } 356 357 public int getImageWidth(int xIndex) { 358 return getImageX(xIndex + 1) - getImageX(xIndex); 359 } 360 361 public int getImageHeight(int yIndex) { 362 return getImageY(yIndex + 1) - getImageY(yIndex); 363 } 364 365 /** 366 * 367 * @return Size of image in original zoom 368 */ 369 public int getBaseImageWidth() { 370 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_EAST.get() * imageSize / 100) : 0; 371 return imageSize + overlap; 372 } 373 374 /** 375 * 376 * @return Size of image in original zoom 377 */ 378 public int getBaseImageHeight() { 379 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_NORTH.get() * imageSize / 100) : 0; 380 return imageSize + overlap; 381 } 382 383 public int getImageSize() { 384 return imageSize; 385 } 386 387 public boolean isOverlapEnabled() { 388 return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0); 389 } 390 391 /** 392 * 393 * @return When overlapping is enabled, return visible part of tile. Otherwise return original image 394 */ 395 public BufferedImage normalizeImage(BufferedImage img) { 396 if (isOverlapEnabled()) { 397 BufferedImage copy = img; 398 img = new BufferedImage(imageSize, imageSize, copy.getType()); 399 img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize, 400 0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null); 401 } 402 return img; 403 } 404 405 /** 406 * 407 * @param xIndex 408 * @param yIndex 409 * @return Real EastNorth of given tile. dx/dy is not counted in 410 */ 411 public EastNorth getEastNorth(int xIndex, int yIndex) { 412 return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree()); 413 } 414 415 protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){ 416 417 int newDax = dax; 418 int newDay = day; 419 420 if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) { 421 newDax = ((bmaxx - bminx) / daStep + 1) * daStep; 422 } 423 424 if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) { 425 newDay = ((bmaxy - bminy) / daStep + 1) * daStep; 426 } 427 428 if (newDax != dax || newDay != day) { 429 dax = newDax; 430 day = newDay; 431 initializeImages(); 432 } 433 434 for(int x = bminx; x<=bmaxx; ++x) { 435 for(int y = bminy; y<=bmaxy; ++y){ 436 images[modulo(x,dax)][modulo(y,day)].changePosition(x, y); 437 } 438 } 439 440 gatherFinishedRequests(); 441 Set<ProjectionBounds> areaToCache = new HashSet<ProjectionBounds>(); 442 443 for(int x = bminx; x<=bmaxx; ++x) { 444 for(int y = bminy; y<=bmaxy; ++y){ 445 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 446 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) { 447 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true); 448 addRequest(request); 449 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 450 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) { 451 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false); 452 addRequest(request); 453 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 454 } 455 } 456 } 457 if (cache != null) { 458 cache.setAreaToCache(areaToCache); 459 } 460 } 461 462 @Override public void visitBoundingBox(BoundingXYVisitor v) { 463 for(int x = 0; x<dax; ++x) { 464 for(int y = 0; y<day; ++y) 465 if(images[x][y].getImage() != null){ 466 v.visit(images[x][y].getMin()); 467 v.visit(images[x][y].getMax()); 468 } 469 } 470 } 471 472 @Override public Action[] getMenuEntries() { 473 return new Action[]{ 474 LayerListDialog.getInstance().createActivateLayerAction(this), 475 LayerListDialog.getInstance().createShowHideLayerAction(), 476 LayerListDialog.getInstance().createDeleteLayerAction(), 477 SeparatorLayerAction.INSTANCE, 478 new OffsetAction(), 479 new LayerSaveAction(this), 480 new LayerSaveAsAction(this), 481 new BookmarkWmsAction(), 482 SeparatorLayerAction.INSTANCE, 483 new ZoomToNativeResolution(), 484 new StartStopAction(), 485 new ToggleAlphaAction(), 486 new ChangeResolutionAction(), 487 new ReloadErrorTilesAction(), 488 new DownloadAction(), 489 SeparatorLayerAction.INSTANCE, 490 new LayerListPopup.InfoAction(this) 491 }; 492 } 493 494 public GeorefImage findImage(EastNorth eastNorth) { 495 int xIndex = getImageXIndex(eastNorth.east()); 496 int yIndex = getImageYIndex(eastNorth.north()); 497 GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)]; 498 if (result.getXIndex() == xIndex && result.getYIndex() == yIndex) 499 return result; 500 else 501 return null; 502 } 503 504 /** 505 * 506 * @param request 507 * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request) 508 */ 509 private int getRequestPriority(WMSRequest request) { 510 if (request.getPixelPerDegree() != info.getPixelPerDegree()) 511 return -1; 512 if (bminx > request.getXIndex() 513 || bmaxx < request.getXIndex() 514 || bminy > request.getYIndex() 515 || bmaxy < request.getYIndex()) 516 return -1; 517 518 MouseEvent lastMEvent = Main.map.mapView.lastMEvent; 519 EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY()); 520 int mouseX = getImageXIndex(cursorEastNorth.east()); 521 int mouseY = getImageYIndex(cursorEastNorth.north()); 522 int dx = request.getXIndex() - mouseX; 523 int dy = request.getYIndex() - mouseY; 524 525 return 1 + dx * dx + dy * dy; 526 } 527 528 private void sortRequests(boolean localOnly) { 529 Iterator<WMSRequest> it = requestQueue.iterator(); 530 while (it.hasNext()) { 531 WMSRequest item = it.next(); 532 533 if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) { 534 it.remove(); 535 continue; 536 } 537 538 int priority = getRequestPriority(item); 539 if (priority == -1 && item.isPrecacheOnly()) { 540 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view 541 } 542 543 if (localOnly && !item.hasExactMatch()) { 544 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately 545 } 546 547 if ( priority == -1 548 || finishedRequests.contains(item) 549 || processingRequests.contains(item)) { 550 it.remove(); 551 } else { 552 item.setPriority(priority); 553 } 554 } 555 Collections.sort(requestQueue); 556 } 557 558 public WMSRequest getRequest(boolean localOnly) { 559 requestQueueLock.lock(); 560 try { 561 workingThreadCount--; 562 563 sortRequests(localOnly); 564 while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) { 565 try { 566 queueEmpty.await(); 567 sortRequests(localOnly); 568 } catch (InterruptedException e) { 569 // Shouldn't happen 570 } 571 } 572 573 workingThreadCount++; 574 if (canceled) 575 return null; 576 else { 577 WMSRequest request = requestQueue.remove(0); 578 processingRequests.add(request); 579 return request; 580 } 581 582 } finally { 583 requestQueueLock.unlock(); 584 } 585 } 586 587 public void finishRequest(WMSRequest request) { 588 requestQueueLock.lock(); 589 try { 590 PrecacheTask task = request.getPrecacheTask(); 591 if (task != null) { 592 task.processedCount++; 593 if (!task.progressMonitor.isCanceled()) { 594 task.progressMonitor.worked(1); 595 task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount)); 596 } 597 } 598 processingRequests.remove(request); 599 if (request.getState() != null && !request.isPrecacheOnly()) { 600 finishedRequests.add(request); 601 Main.map.mapView.repaint(); 602 } 603 } finally { 604 requestQueueLock.unlock(); 605 } 606 } 607 608 public void addRequest(WMSRequest request) { 609 requestQueueLock.lock(); 610 try { 611 612 if (cache != null) { 613 ProjectionBounds b = getBounds(request); 614 // Checking for exact match is fast enough, no need to do it in separated thread 615 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)); 616 if (request.isPrecacheOnly() && request.hasExactMatch()) 617 return; // We already have this tile cached 618 } 619 620 if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) { 621 requestQueue.add(request); 622 if (request.getPrecacheTask() != null) { 623 request.getPrecacheTask().totalCount++; 624 } 625 queueEmpty.signalAll(); 626 } 627 } finally { 628 requestQueueLock.unlock(); 629 } 630 } 631 632 public boolean requestIsVisible(WMSRequest request) { 633 return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex(); 634 } 635 636 private void gatherFinishedRequests() { 637 requestQueueLock.lock(); 638 try { 639 for (WMSRequest request: finishedRequests) { 640 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)]; 641 if (img.equalPosition(request.getXIndex(), request.getYIndex())) { 642 img.changeImage(request.getState(), request.getImage()); 643 } 644 } 645 } finally { 646 requestQueueLock.unlock(); 647 finishedRequests.clear(); 648 } 649 } 650 651 public class DownloadAction extends AbstractAction { 652 public DownloadAction() { 653 super(tr("Download visible tiles")); 654 } 655 @Override 656 public void actionPerformed(ActionEvent ev) { 657 if (zoomIsTooBig()) { 658 JOptionPane.showMessageDialog( 659 Main.parent, 660 tr("The requested area is too big. Please zoom in a little, or change resolution"), 661 tr("Error"), 662 JOptionPane.ERROR_MESSAGE 663 ); 664 } else { 665 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true); 666 } 667 } 668 } 669 670 public static class ChangeResolutionAction extends AbstractAction implements LayerAction { 671 public ChangeResolutionAction() { 672 super(tr("Change resolution")); 673 } 674 675 private void changeResolution(WMSLayer layer) { 676 layer.resolution = Main.map.mapView.getDist100PixelText(); 677 layer.info.setPixelPerDegree(layer.getPPD()); 678 layer.settingsChanged = true; 679 for(int x = 0; x<layer.dax; ++x) { 680 for(int y = 0; y<layer.day; ++y) { 681 layer.images[x][y].changePosition(-1, -1); 682 } 683 } 684 } 685 686 @Override 687 public void actionPerformed(ActionEvent ev) { 688 689 if (LayerListDialog.getInstance() == null) 690 return; 691 692 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 693 for (Layer l: layers) { 694 changeResolution((WMSLayer) l); 695 } 696 Main.map.mapView.repaint(); 697 } 698 699 @Override 700 public boolean supportLayers(List<Layer> layers) { 701 for (Layer l: layers) { 702 if (!(l instanceof WMSLayer)) 703 return false; 704 } 705 return true; 706 } 707 708 @Override 709 public Component createMenuComponent() { 710 return new JMenuItem(this); 711 } 712 713 @Override 714 public boolean equals(Object obj) { 715 return obj instanceof ChangeResolutionAction; 716 } 717 } 718 719 public class ReloadErrorTilesAction extends AbstractAction { 720 public ReloadErrorTilesAction() { 721 super(tr("Reload erroneous tiles")); 722 } 723 @Override 724 public void actionPerformed(ActionEvent ev) { 725 // Delete small files, because they're probably blank tiles. 726 // See https://josm.openstreetmap.de/ticket/2307 727 cache.cleanSmallFiles(4096); 728 729 for (int x = 0; x < dax; ++x) { 730 for (int y = 0; y < day; ++y) { 731 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 732 if(img.getState() == State.FAILED){ 733 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false)); 734 } 735 } 736 } 737 } 738 } 739 740 public class ToggleAlphaAction extends AbstractAction implements LayerAction { 741 public ToggleAlphaAction() { 742 super(tr("Alpha channel")); 743 } 744 @Override 745 public void actionPerformed(ActionEvent ev) { 746 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 747 boolean alphaChannel = checkbox.isSelected(); 748 PROP_ALPHA_CHANNEL.put(alphaChannel); 749 750 // clear all resized cached instances and repaint the layer 751 for (int x = 0; x < dax; ++x) { 752 for (int y = 0; y < day; ++y) { 753 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 754 img.flushedResizedCachedInstance(); 755 } 756 } 757 Main.map.mapView.repaint(); 758 } 759 @Override 760 public Component createMenuComponent() { 761 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 762 item.setSelected(PROP_ALPHA_CHANNEL.get()); 763 return item; 764 } 765 @Override 766 public boolean supportLayers(List<Layer> layers) { 767 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 768 } 769 } 770 771 /** 772 * This action will add a WMS layer menu entry with the current WMS layer 773 * URL and name extended by the current resolution. 774 * When using the menu entry again, the WMS cache will be used properly. 775 */ 776 public class BookmarkWmsAction extends AbstractAction { 777 public BookmarkWmsAction() { 778 super(tr("Set WMS Bookmark")); 779 } 780 @Override 781 public void actionPerformed(ActionEvent ev) { 782 ImageryLayerInfo.addLayer(new ImageryInfo(info)); 783 } 784 } 785 786 private class StartStopAction extends AbstractAction implements LayerAction { 787 788 public StartStopAction() { 789 super(tr("Automatic downloading")); 790 } 791 792 @Override 793 public Component createMenuComponent() { 794 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 795 item.setSelected(autoDownloadEnabled); 796 return item; 797 } 798 799 @Override 800 public boolean supportLayers(List<Layer> layers) { 801 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 802 } 803 804 @Override 805 public void actionPerformed(ActionEvent e) { 806 autoDownloadEnabled = !autoDownloadEnabled; 807 if (autoDownloadEnabled) { 808 for (int x = 0; x < dax; ++x) { 809 for (int y = 0; y < day; ++y) { 810 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 811 if(img.getState() == State.NOT_IN_CACHE){ 812 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true)); 813 } 814 } 815 } 816 Main.map.mapView.repaint(); 817 } 818 } 819 } 820 821 private class ZoomToNativeResolution extends AbstractAction { 822 823 public ZoomToNativeResolution() { 824 super(tr("Zoom to native resolution")); 825 } 826 827 @Override 828 public void actionPerformed(ActionEvent e) { 829 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree()); 830 } 831 832 } 833 834 private void cancelGrabberThreads(boolean wait) { 835 requestQueueLock.lock(); 836 try { 837 canceled = true; 838 for (Grabber grabber: grabbers) { 839 grabber.cancel(); 840 } 841 queueEmpty.signalAll(); 842 } finally { 843 requestQueueLock.unlock(); 844 } 845 if (wait) { 846 for (Thread t: grabberThreads) { 847 try { 848 t.join(); 849 } catch (InterruptedException e) { 850 // Shouldn't happen 851 e.printStackTrace(); 852 } 853 } 854 } 855 } 856 857 private void startGrabberThreads() { 858 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get(); 859 requestQueueLock.lock(); 860 try { 861 canceled = false; 862 grabbers.clear(); 863 grabberThreads.clear(); 864 for (int i=0; i<threadCount; i++) { 865 Grabber grabber = getGrabber(i == 0 && threadCount > 1); 866 grabbers.add(grabber); 867 Thread t = new Thread(grabber, "WMS " + getName() + " " + i); 868 t.setDaemon(true); 869 t.start(); 870 grabberThreads.add(t); 871 } 872 this.workingThreadCount = grabbers.size(); 873 this.threadCount = grabbers.size(); 874 } finally { 875 requestQueueLock.unlock(); 876 } 877 } 878 879 @Override 880 public boolean isChanged() { 881 requestQueueLock.lock(); 882 try { 883 return !finishedRequests.isEmpty() || settingsChanged; 884 } finally { 885 requestQueueLock.unlock(); 886 } 887 } 888 889 @Override 890 public void preferenceChanged(PreferenceChangeEvent event) { 891 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) { 892 cancelGrabberThreads(true); 893 startGrabberThreads(); 894 } else if ( 895 event.getKey().equals(PROP_OVERLAP.getKey()) 896 || event.getKey().equals(PROP_OVERLAP_EAST.getKey()) 897 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) { 898 for (int i=0; i<images.length; i++) { 899 for (int k=0; k<images[i].length; k++) { 900 images[i][k] = new GeorefImage(this); 901 } 902 } 903 904 settingsChanged = true; 905 } 906 } 907 908 protected Grabber getGrabber(boolean localOnly) { 909 if (getInfo().getImageryType() == ImageryType.HTML) 910 return new HTMLGrabber(Main.map.mapView, this, localOnly); 911 else if (getInfo().getImageryType() == ImageryType.WMS) 912 return new WMSGrabber(Main.map.mapView, this, localOnly); 913 else throw new IllegalStateException("getGrabber() called for non-WMS layer type"); 914 } 915 916 public ProjectionBounds getBounds(WMSRequest request) { 917 ProjectionBounds result = new ProjectionBounds( 918 getEastNorth(request.getXIndex(), request.getYIndex()), 919 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1)); 920 921 if (WMSLayer.PROP_OVERLAP.get()) { 922 double eastSize = result.maxEast - result.minEast; 923 double northSize = result.maxNorth - result.minNorth; 924 925 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0; 926 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0; 927 928 result = new ProjectionBounds(result.getMin(), 929 new EastNorth(result.maxEast + eastCoef * eastSize, 930 result.maxNorth + northCoef * northSize)); 931 } 932 return result; 933 } 934 935 @Override 936 public boolean isProjectionSupported(Projection proj) { 937 List<String> serverProjections = info.getServerProjections(); 938 return serverProjections.contains(proj.toCode().toUpperCase()) 939 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84"))) 940 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84")); 941 } 942 943 @Override 944 public String nameSupportedProjections() { 945 String res = ""; 946 for(String p : info.getServerProjections()) { 947 if(!res.isEmpty()) { 948 res += ", "; 949 } 950 res += p; 951 } 952 return tr("Supported projections are: {0}", res); 953 } 954 955 @Override 956 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 957 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0); 958 Main.map.repaint(done ? 0 : 100); 959 return !done; 960 } 961 962 @Override 963 public void writeExternal(ObjectOutput out) throws IOException { 964 out.writeInt(serializeFormatVersion); 965 out.writeInt(dax); 966 out.writeInt(day); 967 out.writeInt(imageSize); 968 out.writeDouble(info.getPixelPerDegree()); 969 out.writeObject(info.getName()); 970 out.writeObject(info.getExtendedUrl()); 971 out.writeObject(images); 972 } 973 974 @Override 975 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 976 int sfv = in.readInt(); 977 if (sfv != serializeFormatVersion) { 978 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion)); 979 } 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 }