Changeset 8526 in josm for trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
- Timestamp:
- 2015-06-24T20:57:43+02:00 (9 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
r8513 r8526 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Component;7 import java.awt.Graphics;8 import java.awt.Graphics2D;9 import java.awt.Image;10 import java.awt.Point;11 6 import java.awt.event.ActionEvent; 12 import java.awt.event.MouseAdapter;13 import java.awt.event.MouseEvent;14 import java.awt.image.BufferedImage;15 import java.awt.image.ImageObserver;16 import java.io.Externalizable;17 import java.io.File;18 7 import java.io.IOException; 19 import java.io.InvalidClassException;20 import java.io.ObjectInput;21 import java.io.ObjectOutput;22 8 import java.util.ArrayList; 23 import java.util.Collections; 24 import java.util.HashSet; 25 import java.util.Iterator; 9 import java.util.Arrays; 26 10 import java.util.List; 27 import java.util.Locale; 28 import java.util.Set; 29 import java.util.concurrent.locks.Condition; 30 import java.util.concurrent.locks.Lock; 31 import java.util.concurrent.locks.ReentrantLock; 11 import java.util.Map; 32 12 33 13 import javax.swing.AbstractAction; 34 14 import javax.swing.Action; 35 import javax.swing.JCheckBoxMenuItem;36 import javax.swing.JMenuItem;37 import javax.swing.JOptionPane;38 15 39 import org.openstreetmap.gui.jmapviewer.AttributionSupport; 16 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 17 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 18 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 40 19 import org.openstreetmap.josm.Main; 41 import org.openstreetmap.josm.actions.SaveActionBase; 42 import org.openstreetmap.josm.data.Bounds; 43 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 44 import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 45 import org.openstreetmap.josm.data.ProjectionBounds; 46 import org.openstreetmap.josm.data.coor.EastNorth; 47 import org.openstreetmap.josm.data.coor.LatLon; 48 import org.openstreetmap.josm.data.imagery.GeorefImage; 49 import org.openstreetmap.josm.data.imagery.GeorefImage.State; 20 import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory; 50 21 import org.openstreetmap.josm.data.imagery.ImageryInfo; 51 22 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 52 23 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 53 import org.openstreetmap.josm.data.imagery. WmsCache;54 import org.openstreetmap.josm.data.imagery. types.ObjectFactory;55 import org.openstreetmap.josm.data. osm.visitor.BoundingXYVisitor;24 import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource; 25 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 26 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 56 27 import org.openstreetmap.josm.data.preferences.BooleanProperty; 57 28 import org.openstreetmap.josm.data.preferences.IntegerProperty; 58 29 import org.openstreetmap.josm.data.projection.Projection; 30 import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 59 31 import org.openstreetmap.josm.gui.MapView; 60 32 import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 61 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;62 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;63 import org.openstreetmap.josm.gui.progress.ProgressMonitor;64 import org.openstreetmap.josm.io.WMSLayerImporter;65 import org.openstreetmap.josm.io.imagery.HTMLGrabber;66 import org.openstreetmap.josm.io.imagery.WMSException;67 import org.openstreetmap.josm.io.imagery.WMSGrabber;68 import org.openstreetmap.josm.io.imagery.WMSRequest;69 import org.openstreetmap.josm.tools.ImageProvider;70 import org.openstreetmap.josm.tools.Utils;71 33 72 34 /** 73 35 * This is a layer that grabs the current screen from an WMS server. The data 74 36 * fetched this way is tiled and managed to the disc to reduce server load. 37 * 75 38 */ 76 public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener, Externalizable { 77 78 public static class PrecacheTask { 79 private final ProgressMonitor progressMonitor; 80 private volatile int totalCount; 81 private volatile int processedCount; 82 private volatile boolean isCancelled; 83 84 public PrecacheTask(ProgressMonitor progressMonitor) { 85 this.progressMonitor = progressMonitor; 86 } 87 88 public boolean isFinished() { 89 return totalCount == processedCount; 90 } 91 92 public int getTotalCount() { 93 return totalCount; 94 } 95 96 public void cancel() { 97 isCancelled = true; 98 } 99 } 100 101 // Fake reference to keep build scripts from removing ObjectFactory class. This class is not used directly but it's necessary for jaxb to work 102 @SuppressWarnings("unused") 103 private static final ObjectFactory OBJECT_FACTORY = null; 104 105 // these values correspond to the zoom levels used throughout OSM and are in meters/pixel from zoom level 0 to 18. 106 // taken from http://wiki.openstreetmap.org/wiki/Zoom_levels 107 private static final Double[] snapLevels = {156412.0, 78206.0, 39103.0, 19551.0, 9776.0, 4888.0, 108 2444.0, 1222.0, 610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547, 4.773, 2.387, 1.193, 0.596}; 109 110 public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true); 111 public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3); 112 public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false); 113 public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14); 114 public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4); 115 public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 500); 39 public class WMSLayer extends AbstractTileSourceLayer { 40 /** default tile size for WMS Layer */ 41 public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 512); 42 /** should WMS layer autozoom in default mode */ 116 43 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true); 117 118 public int messageNum = 5; //limit for messages per layer119 protected double resolution;120 protected String resolutionText;121 protected int imageSize;122 protected int dax = 10;123 protected int day = 10;124 protected int daStep = 5;125 protected int minZoom = 3;126 127 protected GeorefImage[][] images;128 protected static final int serializeFormatVersion = 5;129 protected boolean autoDownloadEnabled = true;130 protected boolean autoResolutionEnabled = PROP_DEFAULT_AUTOZOOM.get();131 protected boolean settingsChanged;132 public transient WmsCache cache;133 private transient AttributionSupport attribution = new AttributionSupport();134 135 // Image index boundary for current view136 private volatile int bminx;137 private volatile int bminy;138 private volatile int bmaxx;139 private volatile int bmaxy;140 private volatile int leftEdge;141 private volatile int bottomEdge;142 143 // Request queue144 private final transient List<WMSRequest> requestQueue = new ArrayList<>();145 private final transient List<WMSRequest> finishedRequests = new ArrayList<>();146 /**147 * List of request currently being processed by download threads148 */149 private final transient List<WMSRequest> processingRequests = new ArrayList<>();150 private final transient Lock requestQueueLock = new ReentrantLock();151 private final transient Condition queueEmpty = requestQueueLock.newCondition();152 private final transient List<WMSGrabber> grabbers = new ArrayList<>();153 private final transient List<Thread> grabberThreads = new ArrayList<>();154 private boolean canceled;155 156 /** set to true if this layer uses an invalid base url */157 private boolean usesInvalidUrl = false;158 /** set to true if the user confirmed to use an potentially invalid WMS base url */159 private boolean isInvalidUrlConfirmed = false;160 44 161 45 /** 162 46 * Constructs a new {@code WMSLayer}. 163 */ 164 public WMSLayer() { 165 this(new ImageryInfo(tr("Blank Layer"))); 166 } 167 168 /** 169 * Constructs a new {@code WMSLayer}. 47 * @param info ImageryInfo description of the layer 170 48 */ 171 49 public WMSLayer(ImageryInfo info) { 172 50 super(info); 173 imageSize = PROP_IMAGE_SIZE.get();174 setBackgroundLayer(true); /* set global background variable */175 initializeImages();176 177 attribution.initialize(this.info);178 179 Main.pref.addPreferenceChangeListener(this);180 51 } 181 52 182 53 @Override 183 54 public void hookUpMapView() { 184 if (info.getUrl() != null) { 185 startGrabberThreads(); 55 super.hookUpMapView(); 56 final ProjectionChangeListener listener = new ProjectionChangeListener() { 57 @Override 58 public void projectionChanged(Projection oldValue, Projection newValue) { 59 if (!oldValue.equals(newValue) && tileSource instanceof TemplatedWMSTileSource) { 60 ((TemplatedWMSTileSource)tileSource).initProjection(newValue); 61 } 186 62 187 for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {188 if (layer.getInfo().getUrl().equals(info.getUrl())) {189 cache = layer.cache;190 break;191 }192 }193 if (cache == null) {194 cache = new WmsCache(info.getUrl(), imageSize);195 cache.loadIndex();196 }197 }198 199 // if automatic resolution is enabled, ensure that the first zoom level200 // is already snapped. Otherwise it may load tiles that will never get201 // used again when zooming.202 updateResolutionSetting(this, autoResolutionEnabled);203 204 final MouseAdapter adapter = new MouseAdapter() {205 @Override206 public void mouseClicked(MouseEvent e) {207 if (!isVisible()) return;208 if (e.getButton() == MouseEvent.BUTTON1) {209 attribution.handleAttribution(e.getPoint(), true);210 }211 63 } 212 64 }; 213 Main. map.mapView.addMouseListener(adapter);65 Main.addProjectionChangeListener(listener); 214 66 215 67 MapView.addLayerChangeListener(new LayerChangeListener() { 216 68 @Override 217 69 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 218 // 70 // empty 219 71 } 220 72 221 73 @Override 222 74 public void layerAdded(Layer newLayer) { 223 // 75 // empty 224 76 } 225 77 … … 227 79 public void layerRemoved(Layer oldLayer) { 228 80 if (oldLayer == WMSLayer.this) { 229 Main.map.mapView.removeMouseListener(adapter); 230 MapView.removeLayerChangeListener(this); 81 Main.removeProjectionChangeListener(listener); 231 82 } 232 83 } … … 234 85 } 235 86 236 public void doSetName(String name) { 237 setName(name); 238 info.setName(name); 87 @Override 88 public Action[] getMenuEntries() { 89 List<Action> ret = new ArrayList<>(); 90 ret.addAll(Arrays.asList(super.getMenuEntries())); 91 ret.add(SeparatorLayerAction.INSTANCE); 92 ret.add(new LayerSaveAction(this)); 93 ret.add(new LayerSaveAsAction(this)); 94 ret.add(new BookmarkWmsAction()); 95 return ret.toArray(new Action[]{}); 239 96 } 240 97 241 public boolean hasAutoDownload() {242 return autoDownloadEnabled;243 }244 245 public void setAutoDownload(boolean val) {246 autoDownloadEnabled = val;247 }248 249 public boolean isAutoResolution() {250 return autoResolutionEnabled;251 }252 253 public void setAutoResolution(boolean val) {254 autoResolutionEnabled = val;255 }256 257 public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {258 Set<Point> requestedTiles = new HashSet<>();259 for (LatLon point: points) {260 EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));261 EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));262 int minX = getImageXIndex(minEn.east());263 int maxX = getImageXIndex(maxEn.east());264 int minY = getImageYIndex(minEn.north());265 int maxY = getImageYIndex(maxEn.north());266 267 for (int x = minX; x <= maxX; x++) {268 for (int y = minY; y <= maxY; y++) {269 requestedTiles.add(new Point(x, y));270 }271 }272 }273 274 for (Point p: requestedTiles) {275 addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));276 }277 278 precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());279 precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));280 }281 98 282 99 @Override 283 p ublic void destroy(){284 super.destroy();285 cancelGrabberThreads(false);286 Main.pref.removePreferenceChangeListener(this);287 if (cache != null) {288 cache.saveIndex();100 protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException { 101 if (info.getImageryType() == ImageryType.WMS && info.getUrl() != null) { 102 TemplatedWMSTileSource.checkUrl(info.getUrl()); 103 TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info); 104 info.setAttribution(tileSource); 105 return tileSource; 289 106 } 290 } 291 292 public final void initializeImages() { 293 GeorefImage[][] old = images; 294 images = new GeorefImage[dax][day]; 295 if (old != null) { 296 for (GeorefImage[] row : old) { 297 for (GeorefImage image : row) { 298 images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image; 299 } 300 } 301 } 302 for (int x = 0; x < dax; ++x) { 303 for (int y = 0; y < day; ++y) { 304 if (images[x][y] == null) { 305 images[x][y] = new GeorefImage(this); 306 } 307 } 308 } 309 } 310 311 @Override public ImageryInfo getInfo() { 312 return info; 313 } 314 315 @Override public String getToolTipText() { 316 if (autoDownloadEnabled) 317 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText); 318 else 319 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText); 320 } 321 322 private int modulo(int a, int b) { 323 return a % b >= 0 ? a % b : a % b+b; 324 } 325 326 private boolean zoomIsTooBig() { 327 //don't download when it's too outzoomed 328 return info.getPixelPerDegree() / getPPD() > minZoom; 329 } 330 331 @Override 332 public void paint(Graphics2D g, final MapView mv, Bounds b) { 333 if (info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return; 334 335 if (autoResolutionEnabled && !Utils.equalsEpsilon(getBestZoom(), mv.getDist100Pixel())) { 336 changeResolution(this, true); 337 } 338 339 settingsChanged = false; 340 341 ProjectionBounds bounds = mv.getProjectionBounds(); 342 bminx = getImageXIndex(bounds.minEast); 343 bminy = getImageYIndex(bounds.minNorth); 344 bmaxx = getImageXIndex(bounds.maxEast); 345 bmaxy = getImageYIndex(bounds.maxNorth); 346 347 leftEdge = (int) (bounds.minEast * getPPD()); 348 bottomEdge = (int) (bounds.minNorth * getPPD()); 349 350 if (zoomIsTooBig()) { 351 for (int x = 0; x < images.length; ++x) { 352 for (int y = 0; y < images[0].length; ++y) { 353 GeorefImage image = images[x][y]; 354 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge); 355 } 356 } 357 } else { 358 downloadAndPaintVisible(g, mv, false); 359 } 360 361 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this); 362 } 363 364 @Override 365 public void setOffset(double dx, double dy) { 366 super.setOffset(dx, dy); 367 settingsChanged = true; 368 } 369 370 public int getImageXIndex(double coord) { 371 return (int) Math.floor(((coord - dx) * info.getPixelPerDegree()) / imageSize); 372 } 373 374 public int getImageYIndex(double coord) { 375 return (int) Math.floor(((coord - dy) * info.getPixelPerDegree()) / imageSize); 376 } 377 378 public int getImageX(int imageIndex) { 379 return (int) (imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD()); 380 } 381 382 public int getImageY(int imageIndex) { 383 return (int) (imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD()); 384 } 385 386 public int getImageWidth(int xIndex) { 387 return getImageX(xIndex + 1) - getImageX(xIndex); 388 } 389 390 public int getImageHeight(int yIndex) { 391 return getImageY(yIndex + 1) - getImageY(yIndex); 392 } 393 394 /** 395 * 396 * @return Size of image in original zoom 397 */ 398 public int getBaseImageWidth() { 399 int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_EAST.get() * imageSize / 100 : 0; 400 return imageSize + overlap; 401 } 402 403 /** 404 * 405 * @return Size of image in original zoom 406 */ 407 public int getBaseImageHeight() { 408 int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_NORTH.get() * imageSize / 100 : 0; 409 return imageSize + overlap; 410 } 411 412 public int getImageSize() { 413 return imageSize; 414 } 415 416 public boolean isOverlapEnabled() { 417 return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0); 418 } 419 420 /** 421 * 422 * @return When overlapping is enabled, return visible part of tile. Otherwise return original image 423 */ 424 public BufferedImage normalizeImage(BufferedImage img) { 425 if (isOverlapEnabled()) { 426 BufferedImage copy = img; 427 img = new BufferedImage(imageSize, imageSize, copy.getType()); 428 img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize, 429 0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null); 430 } 431 return img; 432 } 433 434 /** 435 * Returns east/north for a x/y couple. 436 * @param xIndex x index 437 * @param yIndex y index 438 * @return Real EastNorth of given tile. dx/dy is not counted in 439 */ 440 public EastNorth getEastNorth(int xIndex, int yIndex) { 441 return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree()); 442 } 443 444 protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real) { 445 446 int newDax = dax; 447 int newDay = day; 448 449 if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) { 450 newDax = ((bmaxx - bminx) / daStep + 1) * daStep; 451 } 452 453 if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) { 454 newDay = ((bmaxy - bminy) / daStep + 1) * daStep; 455 } 456 457 if (newDax != dax || newDay != day) { 458 dax = newDax; 459 day = newDay; 460 initializeImages(); 461 } 462 463 for (int x = bminx; x <= bmaxx; ++x) { 464 for (int y = bminy; y <= bmaxy; ++y) { 465 images[modulo(x, dax)][modulo(y, day)].changePosition(x, y); 466 } 467 } 468 469 gatherFinishedRequests(); 470 Set<ProjectionBounds> areaToCache = new HashSet<>(); 471 472 for (int x = bminx; x <= bmaxx; ++x) { 473 for (int y = bminy; y <= bmaxy; ++y) { 474 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 475 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) { 476 addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, true)); 477 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 478 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) { 479 addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, false)); 480 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 481 } 482 } 483 } 484 if (cache != null) { 485 cache.setAreaToCache(areaToCache); 486 } 487 } 488 489 @Override 490 public void visitBoundingBox(BoundingXYVisitor v) { 491 for (int x = 0; x < dax; ++x) { 492 for (int y = 0; y < day; ++y) { 493 if (images[x][y].getImage() != null) { 494 v.visit(images[x][y].getMin()); 495 v.visit(images[x][y].getMax()); 496 } 497 } 498 } 499 } 500 501 @Override 502 public Action[] getMenuEntries() { 503 return new Action[]{ 504 LayerListDialog.getInstance().createActivateLayerAction(this), 505 LayerListDialog.getInstance().createShowHideLayerAction(), 506 LayerListDialog.getInstance().createDeleteLayerAction(), 507 SeparatorLayerAction.INSTANCE, 508 new OffsetAction(), 509 new LayerSaveAction(this), 510 new LayerSaveAsAction(this), 511 new BookmarkWmsAction(), 512 SeparatorLayerAction.INSTANCE, 513 new StartStopAction(), 514 new ToggleAlphaAction(), 515 new ToggleAutoResolutionAction(), 516 new ChangeResolutionAction(), 517 new ZoomToNativeResolution(), 518 new ReloadErrorTilesAction(), 519 new DownloadAction(), 520 SeparatorLayerAction.INSTANCE, 521 new LayerListPopup.InfoAction(this) 522 }; 523 } 524 525 public GeorefImage findImage(EastNorth eastNorth) { 526 int xIndex = getImageXIndex(eastNorth.east()); 527 int yIndex = getImageYIndex(eastNorth.north()); 528 GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)]; 529 if (result.getXIndex() == xIndex && result.getYIndex() == yIndex) 530 return result; 531 else 532 return null; 533 } 534 535 /** 536 * Replies request priority. 537 * @param request WMS request 538 * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request) 539 */ 540 private int getRequestPriority(WMSRequest request) { 541 if (!Utils.equalsEpsilon(request.getPixelPerDegree(), info.getPixelPerDegree())) 542 return -1; 543 if (bminx > request.getXIndex() 544 || bmaxx < request.getXIndex() 545 || bminy > request.getYIndex() 546 || bmaxy < request.getYIndex()) 547 return -1; 548 549 MouseEvent lastMEvent = Main.map.mapView.lastMEvent; 550 EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY()); 551 int mouseX = getImageXIndex(cursorEastNorth.east()); 552 int mouseY = getImageYIndex(cursorEastNorth.north()); 553 int dx = request.getXIndex() - mouseX; 554 int dy = request.getYIndex() - mouseY; 555 556 return 1 + dx * dx + dy * dy; 557 } 558 559 private void sortRequests(boolean localOnly) { 560 Iterator<WMSRequest> it = requestQueue.iterator(); 561 while (it.hasNext()) { 562 WMSRequest item = it.next(); 563 564 if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) { 565 it.remove(); 566 continue; 567 } 568 569 int priority = getRequestPriority(item); 570 if (priority == -1 && item.isPrecacheOnly()) { 571 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view 572 } 573 574 if (localOnly && !item.hasExactMatch()) { 575 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately 576 } 577 578 if (priority == -1 579 || finishedRequests.contains(item) 580 || processingRequests.contains(item)) { 581 it.remove(); 582 } else { 583 item.setPriority(priority); 584 } 585 } 586 Collections.sort(requestQueue); 587 } 588 589 public WMSRequest getRequest(boolean localOnly) { 590 requestQueueLock.lock(); 591 try { 592 sortRequests(localOnly); 593 while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) { 594 try { 595 queueEmpty.await(); 596 sortRequests(localOnly); 597 } catch (InterruptedException e) { 598 Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request"); 599 } 600 } 601 602 if (canceled) 603 return null; 604 else { 605 WMSRequest request = requestQueue.remove(0); 606 processingRequests.add(request); 607 return request; 608 } 609 610 } finally { 611 requestQueueLock.unlock(); 612 } 613 } 614 615 public void finishRequest(WMSRequest request) { 616 requestQueueLock.lock(); 617 try { 618 PrecacheTask task = request.getPrecacheTask(); 619 if (task != null) { 620 task.processedCount++; 621 if (!task.progressMonitor.isCanceled()) { 622 task.progressMonitor.worked(1); 623 task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount)); 624 } 625 } 626 processingRequests.remove(request); 627 if (request.getState() != null && !request.isPrecacheOnly()) { 628 finishedRequests.add(request); 629 if (Main.isDisplayingMapView()) { 630 Main.map.mapView.repaint(); 631 } 632 } 633 } finally { 634 requestQueueLock.unlock(); 635 } 636 } 637 638 public void addRequest(WMSRequest request) { 639 requestQueueLock.lock(); 640 try { 641 642 if (cache != null) { 643 ProjectionBounds b = getBounds(request); 644 // Checking for exact match is fast enough, no need to do it in separated thread 645 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)); 646 if (request.isPrecacheOnly() && request.hasExactMatch()) 647 return; // We already have this tile cached 648 } 649 650 if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) { 651 requestQueue.add(request); 652 if (request.getPrecacheTask() != null) { 653 request.getPrecacheTask().totalCount++; 654 } 655 queueEmpty.signalAll(); 656 } 657 } finally { 658 requestQueueLock.unlock(); 659 } 660 } 661 662 public boolean requestIsVisible(WMSRequest request) { 663 return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex(); 664 } 665 666 private void gatherFinishedRequests() { 667 requestQueueLock.lock(); 668 try { 669 for (WMSRequest request: finishedRequests) { 670 GeorefImage img = images[modulo(request.getXIndex(), dax)][modulo(request.getYIndex(), day)]; 671 if (img.equalPosition(request.getXIndex(), request.getYIndex())) { 672 WMSException we = request.getException(); 673 img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null); 674 } 675 } 676 } finally { 677 requestQueueLock.unlock(); 678 finishedRequests.clear(); 679 } 680 } 681 682 public class DownloadAction extends AbstractAction { 683 /** 684 * Constructs a new {@code DownloadAction}. 685 */ 686 public DownloadAction() { 687 super(tr("Download visible tiles")); 688 } 689 690 @Override 691 public void actionPerformed(ActionEvent ev) { 692 if (zoomIsTooBig()) { 693 JOptionPane.showMessageDialog( 694 Main.parent, 695 tr("The requested area is too big. Please zoom in a little, or change resolution"), 696 tr("Error"), 697 JOptionPane.ERROR_MESSAGE 698 ); 699 } else { 700 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true); 701 } 702 } 703 } 704 705 /** 706 * Finds the most suitable resolution for the current zoom level, but prefers 707 * higher resolutions. Snaps to values defined in snapLevels. 708 * @return best zoom level 709 */ 710 private static double getBestZoom() { 711 // not sure why getDist100Pixel returns values corresponding to 712 // the snapLevels, which are in meters per pixel. It works, though. 713 double dist = Main.map.mapView.getDist100Pixel(); 714 for (int i = snapLevels.length-2; i >= 0; i--) { 715 if (snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist) 716 return snapLevels[i+1]; 717 } 718 return snapLevels[0]; 719 } 720 721 /** 722 * Updates the given layer’s resolution settings to the current zoom level. Does 723 * not update existing tiles, only new ones will be subject to the new settings. 724 * 725 * @param layer WMS layer 726 * @param snap Set to true if the resolution should snap to certain values instead of 727 * matching the current zoom level perfectly 728 */ 729 private static void updateResolutionSetting(WMSLayer layer, boolean snap) { 730 if (snap) { 731 layer.resolution = getBestZoom(); 732 layer.resolutionText = MapView.getDistText(layer.resolution); 733 } else { 734 layer.resolution = Main.map.mapView.getDist100Pixel(); 735 layer.resolutionText = Main.map.mapView.getDist100PixelText(); 736 } 737 layer.info.setPixelPerDegree(layer.getPPD()); 738 } 739 740 /** 741 * Updates the given layer’s resolution settings to the current zoom level and 742 * updates existing tiles. If round is true, tiles will be updated gradually, if 743 * false they will be removed instantly (and redrawn only after the new resolution 744 * image has been loaded). 745 * @param layer WMS layer 746 * @param snap Set to true if the resolution should snap to certain values instead of 747 * matching the current zoom level perfectly 748 */ 749 private static void changeResolution(WMSLayer layer, boolean snap) { 750 updateResolutionSetting(layer, snap); 751 752 layer.settingsChanged = true; 753 754 // Don’t move tiles off screen when the resolution is rounded. This 755 // prevents some flickering when zooming with auto-resolution enabled 756 // and instead gradually updates each tile. 757 if (!snap) { 758 for (int x = 0; x < layer.dax; ++x) { 759 for (int y = 0; y < layer.day; ++y) { 760 layer.images[x][y].changePosition(-1, -1); 761 } 762 } 763 } 764 } 765 766 public static class ChangeResolutionAction extends AbstractAction implements LayerAction { 767 768 /** 769 * Constructs a new {@code ChangeResolutionAction} 770 */ 771 public ChangeResolutionAction() { 772 super(tr("Change resolution")); 773 } 774 775 @Override 776 public void actionPerformed(ActionEvent ev) { 777 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 778 for (Layer l: layers) { 779 changeResolution((WMSLayer) l, false); 780 } 781 Main.map.mapView.repaint(); 782 } 783 784 @Override 785 public boolean supportLayers(List<Layer> layers) { 786 for (Layer l: layers) { 787 if (!(l instanceof WMSLayer)) 788 return false; 789 } 790 return true; 791 } 792 793 @Override 794 public Component createMenuComponent() { 795 return new JMenuItem(this); 796 } 797 } 798 799 public class ReloadErrorTilesAction extends AbstractAction { 800 /** 801 * Constructs a new {@code ReloadErrorTilesAction}. 802 */ 803 public ReloadErrorTilesAction() { 804 super(tr("Reload erroneous tiles")); 805 } 806 807 @Override 808 public void actionPerformed(ActionEvent ev) { 809 // Delete small files, because they're probably blank tiles. 810 // See #2307 811 cache.cleanSmallFiles(4096); 812 813 for (int x = 0; x < dax; ++x) { 814 for (int y = 0; y < day; ++y) { 815 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 816 if (img.getState() == State.FAILED) { 817 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false)); 818 } 819 } 820 } 821 } 822 } 823 824 public class ToggleAlphaAction extends AbstractAction implements LayerAction { 825 /** 826 * Constructs a new {@code ToggleAlphaAction}. 827 */ 828 public ToggleAlphaAction() { 829 super(tr("Alpha channel")); 830 } 831 832 @Override 833 public void actionPerformed(ActionEvent ev) { 834 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 835 boolean alphaChannel = checkbox.isSelected(); 836 PROP_ALPHA_CHANNEL.put(alphaChannel); 837 Main.info("WMS Alpha channel changed to "+alphaChannel); 838 839 // clear all resized cached instances and repaint the layer 840 for (int x = 0; x < dax; ++x) { 841 for (int y = 0; y < day; ++y) { 842 GeorefImage img = images[modulo(x, dax)][modulo(y, day)]; 843 img.flushResizedCachedInstance(); 844 BufferedImage bi = img.getImage(); 845 // Completely erases images for which transparency has been forced, 846 // or images that should be forced now, as they need to be recreated 847 if (ImageProvider.isTransparencyForced(bi) || ImageProvider.hasTransparentColor(bi)) { 848 img.resetImage(); 849 } 850 } 851 } 852 Main.map.mapView.repaint(); 853 } 854 855 @Override 856 public Component createMenuComponent() { 857 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 858 item.setSelected(PROP_ALPHA_CHANNEL.get()); 859 return item; 860 } 861 862 @Override 863 public boolean supportLayers(List<Layer> layers) { 864 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 865 } 866 } 867 868 public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction { 869 870 /** 871 * Constructs a new {@code ToggleAutoResolutionAction}. 872 */ 873 public ToggleAutoResolutionAction() { 874 super(tr("Automatically change resolution")); 875 } 876 877 @Override 878 public void actionPerformed(ActionEvent ev) { 879 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource(); 880 autoResolutionEnabled = checkbox.isSelected(); 881 } 882 883 @Override 884 public Component createMenuComponent() { 885 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 886 item.setSelected(autoResolutionEnabled); 887 return item; 888 } 889 890 @Override 891 public boolean supportLayers(List<Layer> layers) { 892 return layers.size() == 1 && layers.get(0) instanceof WMSLayer; 893 } 107 return null; 894 108 } 895 109 … … 913 127 } 914 128 915 private class StartStopAction extends AbstractAction implements LayerAction {916 917 public StartStopAction() {918 super(tr("Automatic downloading"));919 }920 921 @Override922 public Component createMenuComponent() {923 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);924 item.setSelected(autoDownloadEnabled);925 return item;926 }927 928 @Override929 public boolean supportLayers(List<Layer> layers) {930 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;931 }932 933 @Override934 public void actionPerformed(ActionEvent e) {935 autoDownloadEnabled = !autoDownloadEnabled;936 if (autoDownloadEnabled) {937 for (int x = 0; x < dax; ++x) {938 for (int y = 0; y < day; ++y) {939 GeorefImage img = images[modulo(x, dax)][modulo(y, day)];940 if (img.getState() == State.NOT_IN_CACHE) {941 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));942 }943 }944 }945 Main.map.mapView.repaint();946 }947 }948 }949 950 private class ZoomToNativeResolution extends AbstractAction {951 952 public ZoomToNativeResolution() {953 super(tr("Zoom to native resolution"));954 }955 956 @Override957 public void actionPerformed(ActionEvent e) {958 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());959 }960 }961 962 private void cancelGrabberThreads(boolean wait) {963 requestQueueLock.lock();964 try {965 canceled = true;966 for (WMSGrabber grabber: grabbers) {967 grabber.cancel();968 }969 queueEmpty.signalAll();970 } finally {971 requestQueueLock.unlock();972 }973 if (wait) {974 for (Thread t: grabberThreads) {975 try {976 t.join();977 } catch (InterruptedException e) {978 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");979 }980 }981 }982 }983 984 private void startGrabberThreads() {985 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();986 requestQueueLock.lock();987 try {988 canceled = false;989 grabbers.clear();990 grabberThreads.clear();991 for (int i = 0; i < threadCount; i++) {992 WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);993 grabbers.add(grabber);994 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);995 t.setDaemon(true);996 t.start();997 grabberThreads.add(t);998 }999 } finally {1000 requestQueueLock.unlock();1001 }1002 }1003 1004 @Override1005 public boolean isChanged() {1006 requestQueueLock.lock();1007 try {1008 return !finishedRequests.isEmpty() || settingsChanged;1009 } finally {1010 requestQueueLock.unlock();1011 }1012 }1013 1014 @Override1015 public void preferenceChanged(PreferenceChangeEvent event) {1016 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {1017 cancelGrabberThreads(true);1018 startGrabberThreads();1019 } else if (1020 event.getKey().equals(PROP_OVERLAP.getKey())1021 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())1022 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {1023 for (int i = 0; i < images.length; i++) {1024 for (int k = 0; k < images[i].length; k++) {1025 images[i][k] = new GeorefImage(this);1026 }1027 }1028 1029 settingsChanged = true;1030 }1031 }1032 129 1033 130 /** … … 1037 134 */ 1038 135 public void checkGrabberType() { 1039 ImageryType it = getInfo().getImageryType();1040 if (!ImageryType.HTML.equals(it) && !ImageryType.WMS.equals(it))1041 throw new IllegalStateException("getGrabber() called for non-WMS layer type");1042 136 } 1043 137 1044 protected WMSGrabber getGrabber(boolean localOnly) { 1045 checkGrabberType(); 1046 if (getInfo().getImageryType() == ImageryType.HTML) 1047 return new HTMLGrabber(Main.map.mapView, this, localOnly); 1048 else 1049 return new WMSGrabber(Main.map.mapView, this, localOnly); 1050 } 138 private static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("WMS") { 139 @Override 140 protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, 141 int readTimeout, Map<String, String> headers, String cacheDir) throws IOException { 142 return new WMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir); 143 } 1051 144 1052 public ProjectionBounds getBounds(WMSRequest request) { 1053 ProjectionBounds result = new ProjectionBounds( 1054 getEastNorth(request.getXIndex(), request.getYIndex()), 1055 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1)); 145 }; 1056 146 1057 if (WMSLayer.PROP_OVERLAP.get()) { 1058 double eastSize = result.maxEast - result.minEast; 1059 double northSize = result.maxNorth - result.minNorth; 1060 1061 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0; 1062 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0; 1063 1064 result = new ProjectionBounds(result.getMin(), 1065 new EastNorth(result.maxEast + eastCoef * eastSize, 1066 result.maxNorth + northCoef * northSize)); 1067 } 1068 return result; 147 @Override 148 protected TileLoaderFactory getTileLoaderFactory() { 149 return loaderFactory; 1069 150 } 1070 151 1071 152 @Override 1072 public boolean isProjectionSupported(Projection proj) { 1073 List<String> serverProjections = info.getServerProjections(); 1074 return serverProjections.contains(proj.toCode().toUpperCase(Locale.ENGLISH)) 1075 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84"))) 1076 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84")); 1077 } 1078 1079 @Override 1080 public String nameSupportedProjections() { 1081 StringBuilder res = new StringBuilder(); 1082 for (String p : info.getServerProjections()) { 1083 if (res.length() > 0) { 1084 res.append(", "); 1085 } 1086 res.append(p); 153 protected Map<String, String> getHeaders(TileSource tileSource) { 154 if (tileSource instanceof TemplatedWMSTileSource) { 155 return ((TemplatedWMSTileSource)tileSource).getHeaders(); 1087 156 } 1088 return tr("Supported projections are: {0}", res); 1089 } 1090 1091 @Override 1092 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 1093 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 1094 Main.map.repaint(done ? 0 : 100); 1095 return !done; 1096 } 1097 1098 @Override 1099 public void writeExternal(ObjectOutput out) throws IOException { 1100 out.writeInt(serializeFormatVersion); 1101 out.writeInt(dax); 1102 out.writeInt(day); 1103 out.writeInt(imageSize); 1104 out.writeDouble(info.getPixelPerDegree()); 1105 out.writeObject(info.getName()); 1106 out.writeObject(info.getExtendedUrl()); 1107 out.writeObject(images); 1108 } 1109 1110 @Override 1111 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 1112 int sfv = in.readInt(); 1113 if (sfv != serializeFormatVersion) 1114 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion)); 1115 autoDownloadEnabled = false; 1116 dax = in.readInt(); 1117 day = in.readInt(); 1118 imageSize = in.readInt(); 1119 info.setPixelPerDegree(in.readDouble()); 1120 doSetName((String) in.readObject()); 1121 info.setExtendedUrl((String) in.readObject()); 1122 images = (GeorefImage[][]) in.readObject(); 1123 1124 for (GeorefImage[] imgs : images) { 1125 for (GeorefImage img : imgs) { 1126 if (img != null) { 1127 img.setLayer(WMSLayer.this); 1128 } 1129 } 1130 } 1131 1132 settingsChanged = true; 1133 if (Main.isDisplayingMapView()) { 1134 Main.map.mapView.repaint(); 1135 } 1136 if (cache != null) { 1137 cache.saveIndex(); 1138 cache = null; 1139 } 1140 } 1141 1142 @Override 1143 public void onPostLoadFromFile() { 1144 if (info.getUrl() != null) { 1145 cache = new WmsCache(info.getUrl(), imageSize); 1146 startGrabberThreads(); 1147 } 1148 } 1149 1150 @Override 1151 public boolean isSavable() { 1152 return true; // With WMSLayerExporter 1153 } 1154 1155 @Override 1156 public File createAndOpenSaveFileChooser() { 1157 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 157 return null; 1158 158 } 1159 159 }
Note:
See TracChangeset
for help on using the changeset viewer.