source: josm/trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java@ 7425

Last change on this file since 7425 was 7425, checked in by Don-vip, 10 years ago

fix #3916 - WMS: Improve exception handling. Proper message is now displayed on tiles.

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