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

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

fix #9189 - NPE when deleting data layer + additional usage of Main.isDisplayingMapView()

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