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

Last change on this file since 5969 was 5969, checked in by xeen, 11 years ago

automatically zoom WMS sources (fixes #7563)

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