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

Last change on this file since 7509 was 7509, checked in by stoecker, 10 years ago

remove tabs

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