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

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

code refactoring/cleanup/javadoc + fix bug in preset text comparator in menu

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