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

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

code style - Useless parentheses around expressions should be removed to prevent any misunderstanding

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