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

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

When doing a String.toLowerCase()/toUpperCase() call, use a Locale. This avoids problems with certain locales, i.e. Lithuanian or Turkish. See PMD UseLocaleWithCaseConversions rule and String.toLowerCase() javadoc.

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