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

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

see #8465 - use diamond operator where applicable

  • Property svn:eol-style set to native
File size: 39.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.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 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 public WMSLayer() {
159 this(new ImageryInfo(tr("Blank Layer")));
160 }
161
162 public WMSLayer(ImageryInfo info) {
163 super(info);
164 imageSize = PROP_IMAGE_SIZE.get();
165 setBackgroundLayer(true); /* set global background variable */
166 initializeImages();
167
168 attribution.initialize(this.info);
169
170 Main.pref.addPreferenceChangeListener(this);
171 }
172
173 @Override
174 public void hookUpMapView() {
175 if (info.getUrl() != null) {
176 startGrabberThreads();
177
178 for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {
179 if (layer.getInfo().getUrl().equals(info.getUrl())) {
180 cache = layer.cache;
181 break;
182 }
183 }
184 if (cache == null) {
185 cache = new WmsCache(info.getUrl(), imageSize);
186 cache.loadIndex();
187 }
188 }
189
190 // if automatic resolution is enabled, ensure that the first zoom level
191 // is already snapped. Otherwise it may load tiles that will never get
192 // used again when zooming.
193 updateResolutionSetting(this, autoResolutionEnabled);
194
195 final MouseAdapter adapter = new MouseAdapter() {
196 @Override
197 public void mouseClicked(MouseEvent e) {
198 if (!isVisible()) return;
199 if (e.getButton() == MouseEvent.BUTTON1) {
200 attribution.handleAttribution(e.getPoint(), true);
201 }
202 }
203 };
204 Main.map.mapView.addMouseListener(adapter);
205
206 MapView.addLayerChangeListener(new LayerChangeListener() {
207 @Override
208 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
209 //
210 }
211
212 @Override
213 public void layerAdded(Layer newLayer) {
214 //
215 }
216
217 @Override
218 public void layerRemoved(Layer oldLayer) {
219 if (oldLayer == WMSLayer.this) {
220 Main.map.mapView.removeMouseListener(adapter);
221 MapView.removeLayerChangeListener(this);
222 }
223 }
224 });
225 }
226
227 public void doSetName(String name) {
228 setName(name);
229 info.setName(name);
230 }
231
232 public boolean hasAutoDownload(){
233 return autoDownloadEnabled;
234 }
235
236 public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
237 Set<Point> requestedTiles = new HashSet<>();
238 for (LatLon point: points) {
239 EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));
240 EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));
241 int minX = getImageXIndex(minEn.east());
242 int maxX = getImageXIndex(maxEn.east());
243 int minY = getImageYIndex(minEn.north());
244 int maxY = getImageYIndex(maxEn.north());
245
246 for (int x=minX; x<=maxX; x++) {
247 for (int y=minY; y<=maxY; y++) {
248 requestedTiles.add(new Point(x, y));
249 }
250 }
251 }
252
253 for (Point p: requestedTiles) {
254 addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));
255 }
256
257 precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());
258 precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));
259 }
260
261 @Override
262 public void destroy() {
263 super.destroy();
264 cancelGrabberThreads(false);
265 Main.pref.removePreferenceChangeListener(this);
266 if (cache != null) {
267 cache.saveIndex();
268 }
269 }
270
271 public final void initializeImages() {
272 GeorefImage[][] old = images;
273 images = new GeorefImage[dax][day];
274 if (old != null) {
275 for (GeorefImage[] row : old) {
276 for (GeorefImage image : row) {
277 images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;
278 }
279 }
280 }
281 for(int x = 0; x<dax; ++x) {
282 for(int y = 0; y<day; ++y) {
283 if (images[x][y] == null) {
284 images[x][y]= new GeorefImage(this);
285 }
286 }
287 }
288 }
289
290 @Override public ImageryInfo getInfo() {
291 return info;
292 }
293
294 @Override public String getToolTipText() {
295 if(autoDownloadEnabled)
296 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);
297 else
298 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);
299 }
300
301 private int modulo (int a, int b) {
302 return a % b >= 0 ? a%b : a%b+b;
303 }
304
305 private boolean zoomIsTooBig() {
306 //don't download when it's too outzoomed
307 return info.getPixelPerDegree() / getPPD() > minZoom;
308 }
309
310 @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
311 if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
312
313 if (autoResolutionEnabled && getBestZoom() != mv.getDist100Pixel()) {
314 changeResolution(this, true);
315 }
316
317 settingsChanged = false;
318
319 ProjectionBounds bounds = mv.getProjectionBounds();
320 bminx= getImageXIndex(bounds.minEast);
321 bminy= getImageYIndex(bounds.minNorth);
322 bmaxx= getImageXIndex(bounds.maxEast);
323 bmaxy= getImageYIndex(bounds.maxNorth);
324
325 leftEdge = (int)(bounds.minEast * getPPD());
326 bottomEdge = (int)(bounds.minNorth * getPPD());
327
328 if (zoomIsTooBig()) {
329 for(int x = 0; x<images.length; ++x) {
330 for(int y = 0; y<images[0].length; ++y) {
331 GeorefImage image = images[x][y];
332 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
333 }
334 }
335 } else {
336 downloadAndPaintVisible(g, mv, false);
337 }
338
339 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
340
341 }
342
343 @Override
344 public void setOffset(double dx, double dy) {
345 super.setOffset(dx, dy);
346 settingsChanged = true;
347 }
348
349 public int getImageXIndex(double coord) {
350 return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
351 }
352
353 public int getImageYIndex(double coord) {
354 return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
355 }
356
357 public int getImageX(int imageIndex) {
358 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
359 }
360
361 public int getImageY(int imageIndex) {
362 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
363 }
364
365 public int getImageWidth(int xIndex) {
366 return getImageX(xIndex + 1) - getImageX(xIndex);
367 }
368
369 public int getImageHeight(int yIndex) {
370 return getImageY(yIndex + 1) - getImageY(yIndex);
371 }
372
373 /**
374 *
375 * @return Size of image in original zoom
376 */
377 public int getBaseImageWidth() {
378 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_EAST.get() * imageSize / 100) : 0;
379 return imageSize + overlap;
380 }
381
382 /**
383 *
384 * @return Size of image in original zoom
385 */
386 public int getBaseImageHeight() {
387 int overlap = PROP_OVERLAP.get() ? (PROP_OVERLAP_NORTH.get() * imageSize / 100) : 0;
388 return imageSize + overlap;
389 }
390
391 public int getImageSize() {
392 return imageSize;
393 }
394
395 public boolean isOverlapEnabled() {
396 return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0);
397 }
398
399 /**
400 *
401 * @return When overlapping is enabled, return visible part of tile. Otherwise return original image
402 */
403 public BufferedImage normalizeImage(BufferedImage img) {
404 if (isOverlapEnabled()) {
405 BufferedImage copy = img;
406 img = new BufferedImage(imageSize, imageSize, copy.getType());
407 img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize,
408 0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null);
409 }
410 return img;
411 }
412
413 /**
414 *
415 * @param xIndex
416 * @param yIndex
417 * @return Real EastNorth of given tile. dx/dy is not counted in
418 */
419 public EastNorth getEastNorth(int xIndex, int yIndex) {
420 return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
421 }
422
423 protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
424
425 int newDax = dax;
426 int newDay = day;
427
428 if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
429 newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
430 }
431
432 if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
433 newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
434 }
435
436 if (newDax != dax || newDay != day) {
437 dax = newDax;
438 day = newDay;
439 initializeImages();
440 }
441
442 for(int x = bminx; x<=bmaxx; ++x) {
443 for(int y = bminy; y<=bmaxy; ++y){
444 images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
445 }
446 }
447
448 gatherFinishedRequests();
449 Set<ProjectionBounds> areaToCache = new HashSet<>();
450
451 for(int x = bminx; x<=bmaxx; ++x) {
452 for(int y = bminy; y<=bmaxy; ++y){
453 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
454 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
455 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true);
456 addRequest(request);
457 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
458 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
459 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false);
460 addRequest(request);
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 public DownloadAction() {
661 super(tr("Download visible tiles"));
662 }
663 @Override
664 public void actionPerformed(ActionEvent ev) {
665 if (zoomIsTooBig()) {
666 JOptionPane.showMessageDialog(
667 Main.parent,
668 tr("The requested area is too big. Please zoom in a little, or change resolution"),
669 tr("Error"),
670 JOptionPane.ERROR_MESSAGE
671 );
672 } else {
673 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
674 }
675 }
676 }
677
678 /**
679 * Finds the most suitable resolution for the current zoom level, but prefers
680 * higher resolutions. Snaps to values defined in snapLevels.
681 * @return best zoom level
682 */
683 private static double getBestZoom() {
684 // not sure why getDist100Pixel returns values corresponding to
685 // the snapLevels, which are in meters per pixel. It works, though.
686 double dist = Main.map.mapView.getDist100Pixel();
687 for(int i = snapLevels.length-2; i >= 0; i--) {
688 if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
689 return snapLevels[i+1];
690 }
691 return snapLevels[0];
692 }
693
694 /**
695 * Updates the given layer’s resolution settings to the current zoom level. Does
696 * not update existing tiles, only new ones will be subject to the new settings.
697 *
698 * @param layer
699 * @param snap Set to true if the resolution should snap to certain values instead of
700 * matching the current zoom level perfectly
701 */
702 private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
703 if(snap) {
704 layer.resolution = getBestZoom();
705 layer.resolutionText = MapView.getDistText(layer.resolution);
706 } else {
707 layer.resolution = Main.map.mapView.getDist100Pixel();
708 layer.resolutionText = Main.map.mapView.getDist100PixelText();
709 }
710 layer.info.setPixelPerDegree(layer.getPPD());
711 }
712
713 /**
714 * Updates the given layer’s resolution settings to the current zoom level and
715 * updates existing tiles. If round is true, tiles will be updated gradually, if
716 * false they will be removed instantly (and redrawn only after the new resolution
717 * image has been loaded).
718 * @param layer
719 * @param snap Set to true if the resolution should snap to certain values instead of
720 * matching the current zoom level perfectly
721 */
722 private static void changeResolution(WMSLayer layer, boolean snap) {
723 updateResolutionSetting(layer, snap);
724
725 layer.settingsChanged = true;
726
727 // Don’t move tiles off screen when the resolution is rounded. This
728 // prevents some flickering when zooming with auto-resolution enabled
729 // and instead gradually updates each tile.
730 if(!snap) {
731 for(int x = 0; x<layer.dax; ++x) {
732 for(int y = 0; y<layer.day; ++y) {
733 layer.images[x][y].changePosition(-1, -1);
734 }
735 }
736 }
737 }
738
739 public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
740
741 /**
742 * Constructs a new {@code ChangeResolutionAction}
743 */
744 public ChangeResolutionAction() {
745 super(tr("Change resolution"));
746 }
747
748 @Override
749 public void actionPerformed(ActionEvent ev) {
750 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
751 for (Layer l: layers) {
752 changeResolution((WMSLayer) l, false);
753 }
754 Main.map.mapView.repaint();
755 }
756
757 @Override
758 public boolean supportLayers(List<Layer> layers) {
759 for (Layer l: layers) {
760 if (!(l instanceof WMSLayer))
761 return false;
762 }
763 return true;
764 }
765
766 @Override
767 public Component createMenuComponent() {
768 return new JMenuItem(this);
769 }
770
771 @Override
772 public boolean equals(Object obj) {
773 return obj instanceof ChangeResolutionAction;
774 }
775 }
776
777 public class ReloadErrorTilesAction extends AbstractAction {
778 public ReloadErrorTilesAction() {
779 super(tr("Reload erroneous tiles"));
780 }
781 @Override
782 public void actionPerformed(ActionEvent ev) {
783 // Delete small files, because they're probably blank tiles.
784 // See #2307
785 cache.cleanSmallFiles(4096);
786
787 for (int x = 0; x < dax; ++x) {
788 for (int y = 0; y < day; ++y) {
789 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
790 if(img.getState() == State.FAILED){
791 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));
792 }
793 }
794 }
795 }
796 }
797
798 public class ToggleAlphaAction extends AbstractAction implements LayerAction {
799 public ToggleAlphaAction() {
800 super(tr("Alpha channel"));
801 }
802 @Override
803 public void actionPerformed(ActionEvent ev) {
804 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
805 boolean alphaChannel = checkbox.isSelected();
806 PROP_ALPHA_CHANNEL.put(alphaChannel);
807
808 // clear all resized cached instances and repaint the layer
809 for (int x = 0; x < dax; ++x) {
810 for (int y = 0; y < day; ++y) {
811 GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
812 img.flushedResizedCachedInstance();
813 }
814 }
815 Main.map.mapView.repaint();
816 }
817 @Override
818 public Component createMenuComponent() {
819 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
820 item.setSelected(PROP_ALPHA_CHANNEL.get());
821 return item;
822 }
823 @Override
824 public boolean supportLayers(List<Layer> layers) {
825 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
826 }
827 }
828
829
830 public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
831 public ToggleAutoResolutionAction() {
832 super(tr("Automatically change resolution"));
833 }
834
835 @Override
836 public void actionPerformed(ActionEvent ev) {
837 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
838 autoResolutionEnabled = checkbox.isSelected();
839 }
840
841 @Override
842 public Component createMenuComponent() {
843 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
844 item.setSelected(autoResolutionEnabled);
845 return item;
846 }
847
848 @Override
849 public boolean supportLayers(List<Layer> layers) {
850 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
851 }
852 }
853
854 /**
855 * This action will add a WMS layer menu entry with the current WMS layer
856 * URL and name extended by the current resolution.
857 * When using the menu entry again, the WMS cache will be used properly.
858 */
859 public class BookmarkWmsAction extends AbstractAction {
860 public BookmarkWmsAction() {
861 super(tr("Set WMS Bookmark"));
862 }
863 @Override
864 public void actionPerformed(ActionEvent ev) {
865 ImageryLayerInfo.addLayer(new ImageryInfo(info));
866 }
867 }
868
869 private class StartStopAction extends AbstractAction implements LayerAction {
870
871 public StartStopAction() {
872 super(tr("Automatic downloading"));
873 }
874
875 @Override
876 public Component createMenuComponent() {
877 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
878 item.setSelected(autoDownloadEnabled);
879 return item;
880 }
881
882 @Override
883 public boolean supportLayers(List<Layer> layers) {
884 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
885 }
886
887 @Override
888 public void actionPerformed(ActionEvent e) {
889 autoDownloadEnabled = !autoDownloadEnabled;
890 if (autoDownloadEnabled) {
891 for (int x = 0; x < dax; ++x) {
892 for (int y = 0; y < day; ++y) {
893 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
894 if(img.getState() == State.NOT_IN_CACHE){
895 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
896 }
897 }
898 }
899 Main.map.mapView.repaint();
900 }
901 }
902 }
903
904 private class ZoomToNativeResolution extends AbstractAction {
905
906 public ZoomToNativeResolution() {
907 super(tr("Zoom to native resolution"));
908 }
909
910 @Override
911 public void actionPerformed(ActionEvent e) {
912 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
913 }
914
915 }
916
917 private void cancelGrabberThreads(boolean wait) {
918 requestQueueLock.lock();
919 try {
920 canceled = true;
921 for (Grabber grabber: grabbers) {
922 grabber.cancel();
923 }
924 queueEmpty.signalAll();
925 } finally {
926 requestQueueLock.unlock();
927 }
928 if (wait) {
929 for (Thread t: grabberThreads) {
930 try {
931 t.join();
932 } catch (InterruptedException e) {
933 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");
934 }
935 }
936 }
937 }
938
939 private void startGrabberThreads() {
940 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
941 requestQueueLock.lock();
942 try {
943 canceled = false;
944 grabbers.clear();
945 grabberThreads.clear();
946 for (int i=0; i<threadCount; i++) {
947 Grabber grabber = getGrabber(i == 0 && threadCount > 1);
948 grabbers.add(grabber);
949 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
950 t.setDaemon(true);
951 t.start();
952 grabberThreads.add(t);
953 }
954 } finally {
955 requestQueueLock.unlock();
956 }
957 }
958
959 @Override
960 public boolean isChanged() {
961 requestQueueLock.lock();
962 try {
963 return !finishedRequests.isEmpty() || settingsChanged;
964 } finally {
965 requestQueueLock.unlock();
966 }
967 }
968
969 @Override
970 public void preferenceChanged(PreferenceChangeEvent event) {
971 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {
972 cancelGrabberThreads(true);
973 startGrabberThreads();
974 } else if (
975 event.getKey().equals(PROP_OVERLAP.getKey())
976 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
977 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
978 for (int i=0; i<images.length; i++) {
979 for (int k=0; k<images[i].length; k++) {
980 images[i][k] = new GeorefImage(this);
981 }
982 }
983
984 settingsChanged = true;
985 }
986 }
987
988 protected Grabber getGrabber(boolean localOnly) {
989 if (getInfo().getImageryType() == ImageryType.HTML)
990 return new HTMLGrabber(Main.map.mapView, this, localOnly);
991 else if (getInfo().getImageryType() == ImageryType.WMS)
992 return new WMSGrabber(Main.map.mapView, this, localOnly);
993 else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
994 }
995
996 public ProjectionBounds getBounds(WMSRequest request) {
997 ProjectionBounds result = new ProjectionBounds(
998 getEastNorth(request.getXIndex(), request.getYIndex()),
999 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
1000
1001 if (WMSLayer.PROP_OVERLAP.get()) {
1002 double eastSize = result.maxEast - result.minEast;
1003 double northSize = result.maxNorth - result.minNorth;
1004
1005 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
1006 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
1007
1008 result = new ProjectionBounds(result.getMin(),
1009 new EastNorth(result.maxEast + eastCoef * eastSize,
1010 result.maxNorth + northCoef * northSize));
1011 }
1012 return result;
1013 }
1014
1015 @Override
1016 public boolean isProjectionSupported(Projection proj) {
1017 List<String> serverProjections = info.getServerProjections();
1018 return serverProjections.contains(proj.toCode().toUpperCase())
1019 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
1020 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
1021 }
1022
1023 @Override
1024 public String nameSupportedProjections() {
1025 StringBuilder res = new StringBuilder();
1026 for (String p : info.getServerProjections()) {
1027 if (res.length() > 0) {
1028 res.append(", ");
1029 }
1030 res.append(p);
1031 }
1032 return tr("Supported projections are: {0}", res);
1033 }
1034
1035 @Override
1036 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
1037 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
1038 Main.map.repaint(done ? 0 : 100);
1039 return !done;
1040 }
1041
1042 @Override
1043 public void writeExternal(ObjectOutput out) throws IOException {
1044 out.writeInt(serializeFormatVersion);
1045 out.writeInt(dax);
1046 out.writeInt(day);
1047 out.writeInt(imageSize);
1048 out.writeDouble(info.getPixelPerDegree());
1049 out.writeObject(info.getName());
1050 out.writeObject(info.getExtendedUrl());
1051 out.writeObject(images);
1052 }
1053
1054 @Override
1055 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
1056 int sfv = in.readInt();
1057 if (sfv != serializeFormatVersion)
1058 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
1059 autoDownloadEnabled = false;
1060 dax = in.readInt();
1061 day = in.readInt();
1062 imageSize = in.readInt();
1063 info.setPixelPerDegree(in.readDouble());
1064 doSetName((String)in.readObject());
1065 info.setExtendedUrl((String)in.readObject());
1066 images = (GeorefImage[][])in.readObject();
1067
1068 for (GeorefImage[] imgs : images) {
1069 for (GeorefImage img : imgs) {
1070 if (img != null) {
1071 img.setLayer(WMSLayer.this);
1072 }
1073 }
1074 }
1075
1076 settingsChanged = true;
1077 if (Main.isDisplayingMapView()) {
1078 Main.map.mapView.repaint();
1079 }
1080 if (cache != null) {
1081 cache.saveIndex();
1082 cache = null;
1083 }
1084 }
1085
1086 @Override
1087 public void onPostLoadFromFile() {
1088 if (info.getUrl() != null) {
1089 cache = new WmsCache(info.getUrl(), imageSize);
1090 startGrabberThreads();
1091 }
1092 }
1093
1094 @Override
1095 public boolean isSavable() {
1096 return true; // With WMSLayerExporter
1097 }
1098
1099 @Override
1100 public File createAndOpenSaveFileChooser() {
1101 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1102 }
1103}
Note: See TracBrowser for help on using the repository browser.