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

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

fix compilation warnings with JDK8 - equals() and hashcode() must be overriden in pairs

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