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

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

see #8902 - Small performance enhancements / coding style (patch by shinigami):

  • while -> foreach
  • for -> for each

plus:

  • cleanup of FileDrop class to make it more integrated into JOSM core + remove warnings
  • Property svn:eol-style set to native
File size: 39.6 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 (GeorefImage[] row : old) {
277 for (GeorefImage image : row) {
278 images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;
279 }
280 }
281 }
282 for(int x = 0; x<dax; ++x) {
283 for(int y = 0; y<day; ++y) {
284 if (images[x][y] == null) {
285 images[x][y]= new GeorefImage(this);
286 }
287 }
288 }
289 }
290
291 @Override public ImageryInfo getInfo() {
292 return info;
293 }
294
295 @Override public String getToolTipText() {
296 if(autoDownloadEnabled)
297 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);
298 else
299 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);
300 }
301
302 private int modulo (int a, int b) {
303 return a % b >= 0 ? a%b : a%b+b;
304 }
305
306 private boolean zoomIsTooBig() {
307 //don't download when it's too outzoomed
308 return info.getPixelPerDegree() / getPPD() > minZoom;
309 }
310
311 @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
312 if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
313
314 if (autoResolutionEnabled && getBestZoom() != mv.getDist100Pixel()) {
315 changeResolution(this, true);
316 }
317
318 settingsChanged = false;
319
320 ProjectionBounds bounds = mv.getProjectionBounds();
321 bminx= getImageXIndex(bounds.minEast);
322 bminy= getImageYIndex(bounds.minNorth);
323 bmaxx= getImageXIndex(bounds.maxEast);
324 bmaxy= getImageYIndex(bounds.maxNorth);
325
326 leftEdge = (int)(bounds.minEast * getPPD());
327 bottomEdge = (int)(bounds.minNorth * getPPD());
328
329 if (zoomIsTooBig()) {
330 for(int x = 0; x<images.length; ++x) {
331 for(int y = 0; y<images[0].length; ++y) {
332 GeorefImage image = images[x][y];
333 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge);
334 }
335 }
336 } else {
337 downloadAndPaintVisible(g, mv, false);
338 }
339
340 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);
341
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<ProjectionBounds>();
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 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, true);
457 addRequest(request);
458 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
459 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) {
460 WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real, false);
461 addRequest(request);
462 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1)));
463 }
464 }
465 }
466 if (cache != null) {
467 cache.setAreaToCache(areaToCache);
468 }
469 }
470
471 @Override public void visitBoundingBox(BoundingXYVisitor v) {
472 for(int x = 0; x<dax; ++x) {
473 for(int y = 0; y<day; ++y)
474 if(images[x][y].getImage() != null){
475 v.visit(images[x][y].getMin());
476 v.visit(images[x][y].getMax());
477 }
478 }
479 }
480
481 @Override public Action[] getMenuEntries() {
482 return new Action[]{
483 LayerListDialog.getInstance().createActivateLayerAction(this),
484 LayerListDialog.getInstance().createShowHideLayerAction(),
485 LayerListDialog.getInstance().createDeleteLayerAction(),
486 SeparatorLayerAction.INSTANCE,
487 new OffsetAction(),
488 new LayerSaveAction(this),
489 new LayerSaveAsAction(this),
490 new BookmarkWmsAction(),
491 SeparatorLayerAction.INSTANCE,
492 new StartStopAction(),
493 new ToggleAlphaAction(),
494 new ToggleAutoResolutionAction(),
495 new ChangeResolutionAction(),
496 new ZoomToNativeResolution(),
497 new ReloadErrorTilesAction(),
498 new DownloadAction(),
499 SeparatorLayerAction.INSTANCE,
500 new LayerListPopup.InfoAction(this)
501 };
502 }
503
504 public GeorefImage findImage(EastNorth eastNorth) {
505 int xIndex = getImageXIndex(eastNorth.east());
506 int yIndex = getImageYIndex(eastNorth.north());
507 GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
508 if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)
509 return result;
510 else
511 return null;
512 }
513
514 /**
515 *
516 * @param request
517 * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)
518 */
519 private int getRequestPriority(WMSRequest request) {
520 if (request.getPixelPerDegree() != info.getPixelPerDegree())
521 return -1;
522 if (bminx > request.getXIndex()
523 || bmaxx < request.getXIndex()
524 || bminy > request.getYIndex()
525 || bmaxy < request.getYIndex())
526 return -1;
527
528 MouseEvent lastMEvent = Main.map.mapView.lastMEvent;
529 EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());
530 int mouseX = getImageXIndex(cursorEastNorth.east());
531 int mouseY = getImageYIndex(cursorEastNorth.north());
532 int dx = request.getXIndex() - mouseX;
533 int dy = request.getYIndex() - mouseY;
534
535 return 1 + dx * dx + dy * dy;
536 }
537
538 private void sortRequests(boolean localOnly) {
539 Iterator<WMSRequest> it = requestQueue.iterator();
540 while (it.hasNext()) {
541 WMSRequest item = it.next();
542
543 if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {
544 it.remove();
545 continue;
546 }
547
548 int priority = getRequestPriority(item);
549 if (priority == -1 && item.isPrecacheOnly()) {
550 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view
551 }
552
553 if (localOnly && !item.hasExactMatch()) {
554 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately
555 }
556
557 if ( priority == -1
558 || finishedRequests.contains(item)
559 || processingRequests.contains(item)) {
560 it.remove();
561 } else {
562 item.setPriority(priority);
563 }
564 }
565 Collections.sort(requestQueue);
566 }
567
568 public WMSRequest getRequest(boolean localOnly) {
569 requestQueueLock.lock();
570 try {
571 workingThreadCount--;
572
573 sortRequests(localOnly);
574 while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {
575 try {
576 queueEmpty.await();
577 sortRequests(localOnly);
578 } catch (InterruptedException e) {
579 // Shouldn't happen
580 }
581 }
582
583 workingThreadCount++;
584 if (canceled)
585 return null;
586 else {
587 WMSRequest request = requestQueue.remove(0);
588 processingRequests.add(request);
589 return request;
590 }
591
592 } finally {
593 requestQueueLock.unlock();
594 }
595 }
596
597 public void finishRequest(WMSRequest request) {
598 requestQueueLock.lock();
599 try {
600 PrecacheTask task = request.getPrecacheTask();
601 if (task != null) {
602 task.processedCount++;
603 if (!task.progressMonitor.isCanceled()) {
604 task.progressMonitor.worked(1);
605 task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));
606 }
607 }
608 processingRequests.remove(request);
609 if (request.getState() != null && !request.isPrecacheOnly()) {
610 finishedRequests.add(request);
611 if (Main.map != null && Main.map.mapView != null) {
612 Main.map.mapView.repaint();
613 }
614 }
615 } finally {
616 requestQueueLock.unlock();
617 }
618 }
619
620 public void addRequest(WMSRequest request) {
621 requestQueueLock.lock();
622 try {
623
624 if (cache != null) {
625 ProjectionBounds b = getBounds(request);
626 // Checking for exact match is fast enough, no need to do it in separated thread
627 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));
628 if (request.isPrecacheOnly() && request.hasExactMatch())
629 return; // We already have this tile cached
630 }
631
632 if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {
633 requestQueue.add(request);
634 if (request.getPrecacheTask() != null) {
635 request.getPrecacheTask().totalCount++;
636 }
637 queueEmpty.signalAll();
638 }
639 } finally {
640 requestQueueLock.unlock();
641 }
642 }
643
644 public boolean requestIsVisible(WMSRequest request) {
645 return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
646 }
647
648 private void gatherFinishedRequests() {
649 requestQueueLock.lock();
650 try {
651 for (WMSRequest request: finishedRequests) {
652 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
653 if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
654 img.changeImage(request.getState(), request.getImage());
655 }
656 }
657 } finally {
658 requestQueueLock.unlock();
659 finishedRequests.clear();
660 }
661 }
662
663 public class DownloadAction extends AbstractAction {
664 public DownloadAction() {
665 super(tr("Download visible tiles"));
666 }
667 @Override
668 public void actionPerformed(ActionEvent ev) {
669 if (zoomIsTooBig()) {
670 JOptionPane.showMessageDialog(
671 Main.parent,
672 tr("The requested area is too big. Please zoom in a little, or change resolution"),
673 tr("Error"),
674 JOptionPane.ERROR_MESSAGE
675 );
676 } else {
677 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);
678 }
679 }
680 }
681
682 /**
683 * Finds the most suitable resolution for the current zoom level, but prefers
684 * higher resolutions. Snaps to values defined in snapLevels.
685 * @return
686 */
687 private static double getBestZoom() {
688 // not sure why getDist100Pixel returns values corresponding to
689 // the snapLevels, which are in meters per pixel. It works, though.
690 double dist = Main.map.mapView.getDist100Pixel();
691 for(int i = snapLevels.length-2; i >= 0; i--) {
692 if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)
693 return snapLevels[i+1];
694 }
695 return snapLevels[0];
696 }
697
698 /**
699 * Updates the given layer’s resolution settings to the current zoom level. Does
700 * not update existing tiles, only new ones will be subject to the new settings.
701 *
702 * @param layer
703 * @param snap Set to true if the resolution should snap to certain values instead of
704 * matching the current zoom level perfectly
705 */
706 private static void updateResolutionSetting(WMSLayer layer, boolean snap) {
707 if(snap) {
708 layer.resolution = getBestZoom();
709 layer.resolutionText = MapView.getDistText(layer.resolution);
710 } else {
711 layer.resolution = Main.map.mapView.getDist100Pixel();
712 layer.resolutionText = Main.map.mapView.getDist100PixelText();
713 }
714 layer.info.setPixelPerDegree(layer.getPPD());
715 }
716
717 /**
718 * Updates the given layer’s resolution settings to the current zoom level and
719 * updates existing tiles. If round is true, tiles will be updated gradually, if
720 * false they will be removed instantly (and redrawn only after the new resolution
721 * image has been loaded).
722 * @param layer
723 * @param snap Set to true if the resolution should snap to certain values instead of
724 * matching the current zoom level perfectly
725 */
726 private static void changeResolution(WMSLayer layer, boolean snap) {
727 updateResolutionSetting(layer, snap);
728
729 layer.settingsChanged = true;
730
731 // Don’t move tiles off screen when the resolution is rounded. This
732 // prevents some flickering when zooming with auto-resolution enabled
733 // and instead gradually updates each tile.
734 if(!snap) {
735 for(int x = 0; x<layer.dax; ++x) {
736 for(int y = 0; y<layer.day; ++y) {
737 layer.images[x][y].changePosition(-1, -1);
738 }
739 }
740 }
741 }
742
743
744 public static class ChangeResolutionAction extends AbstractAction implements LayerAction {
745 public ChangeResolutionAction() {
746 super(tr("Change resolution"));
747 }
748
749 @Override
750 public void actionPerformed(ActionEvent ev) {
751
752 if (LayerListDialog.getInstance() == null)
753 return;
754
755 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
756 for (Layer l: layers) {
757 changeResolution((WMSLayer) l, false);
758 }
759 Main.map.mapView.repaint();
760 }
761
762 @Override
763 public boolean supportLayers(List<Layer> layers) {
764 for (Layer l: layers) {
765 if (!(l instanceof WMSLayer))
766 return false;
767 }
768 return true;
769 }
770
771 @Override
772 public Component createMenuComponent() {
773 return new JMenuItem(this);
774 }
775
776 @Override
777 public boolean equals(Object obj) {
778 return obj instanceof ChangeResolutionAction;
779 }
780 }
781
782 public class ReloadErrorTilesAction extends AbstractAction {
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 https://josm.openstreetmap.de/ticket/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 public ToggleAlphaAction() {
805 super(tr("Alpha channel"));
806 }
807 @Override
808 public void actionPerformed(ActionEvent ev) {
809 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
810 boolean alphaChannel = checkbox.isSelected();
811 PROP_ALPHA_CHANNEL.put(alphaChannel);
812
813 // clear all resized cached instances and repaint the layer
814 for (int x = 0; x < dax; ++x) {
815 for (int y = 0; y < day; ++y) {
816 GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
817 img.flushedResizedCachedInstance();
818 }
819 }
820 Main.map.mapView.repaint();
821 }
822 @Override
823 public Component createMenuComponent() {
824 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
825 item.setSelected(PROP_ALPHA_CHANNEL.get());
826 return item;
827 }
828 @Override
829 public boolean supportLayers(List<Layer> layers) {
830 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
831 }
832 }
833
834
835 public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {
836 public ToggleAutoResolutionAction() {
837 super(tr("Automatically change resolution"));
838 }
839
840 @Override
841 public void actionPerformed(ActionEvent ev) {
842 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
843 autoResolutionEnabled = checkbox.isSelected();
844 }
845
846 @Override
847 public Component createMenuComponent() {
848 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
849 item.setSelected(autoResolutionEnabled);
850 return item;
851 }
852
853 @Override
854 public boolean supportLayers(List<Layer> layers) {
855 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
856 }
857 }
858
859 /**
860 * This action will add a WMS layer menu entry with the current WMS layer
861 * URL and name extended by the current resolution.
862 * When using the menu entry again, the WMS cache will be used properly.
863 */
864 public class BookmarkWmsAction extends AbstractAction {
865 public BookmarkWmsAction() {
866 super(tr("Set WMS Bookmark"));
867 }
868 @Override
869 public void actionPerformed(ActionEvent ev) {
870 ImageryLayerInfo.addLayer(new ImageryInfo(info));
871 }
872 }
873
874 private class StartStopAction extends AbstractAction implements LayerAction {
875
876 public StartStopAction() {
877 super(tr("Automatic downloading"));
878 }
879
880 @Override
881 public Component createMenuComponent() {
882 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
883 item.setSelected(autoDownloadEnabled);
884 return item;
885 }
886
887 @Override
888 public boolean supportLayers(List<Layer> layers) {
889 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
890 }
891
892 @Override
893 public void actionPerformed(ActionEvent e) {
894 autoDownloadEnabled = !autoDownloadEnabled;
895 if (autoDownloadEnabled) {
896 for (int x = 0; x < dax; ++x) {
897 for (int y = 0; y < day; ++y) {
898 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
899 if(img.getState() == State.NOT_IN_CACHE){
900 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));
901 }
902 }
903 }
904 Main.map.mapView.repaint();
905 }
906 }
907 }
908
909 private class ZoomToNativeResolution extends AbstractAction {
910
911 public ZoomToNativeResolution() {
912 super(tr("Zoom to native resolution"));
913 }
914
915 @Override
916 public void actionPerformed(ActionEvent e) {
917 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());
918 }
919
920 }
921
922 private void cancelGrabberThreads(boolean wait) {
923 requestQueueLock.lock();
924 try {
925 canceled = true;
926 for (Grabber grabber: grabbers) {
927 grabber.cancel();
928 }
929 queueEmpty.signalAll();
930 } finally {
931 requestQueueLock.unlock();
932 }
933 if (wait) {
934 for (Thread t: grabberThreads) {
935 try {
936 t.join();
937 } catch (InterruptedException e) {
938 // Shouldn't happen
939 e.printStackTrace();
940 }
941 }
942 }
943 }
944
945 private void startGrabberThreads() {
946 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();
947 requestQueueLock.lock();
948 try {
949 canceled = false;
950 grabbers.clear();
951 grabberThreads.clear();
952 for (int i=0; i<threadCount; i++) {
953 Grabber grabber = getGrabber(i == 0 && threadCount > 1);
954 grabbers.add(grabber);
955 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
956 t.setDaemon(true);
957 t.start();
958 grabberThreads.add(t);
959 }
960 this.workingThreadCount = grabbers.size();
961 this.threadCount = grabbers.size();
962 } finally {
963 requestQueueLock.unlock();
964 }
965 }
966
967 @Override
968 public boolean isChanged() {
969 requestQueueLock.lock();
970 try {
971 return !finishedRequests.isEmpty() || settingsChanged;
972 } finally {
973 requestQueueLock.unlock();
974 }
975 }
976
977 @Override
978 public void preferenceChanged(PreferenceChangeEvent event) {
979 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
980 cancelGrabberThreads(true);
981 startGrabberThreads();
982 } else if (
983 event.getKey().equals(PROP_OVERLAP.getKey())
984 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())
985 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {
986 for (int i=0; i<images.length; i++) {
987 for (int k=0; k<images[i].length; k++) {
988 images[i][k] = new GeorefImage(this);
989 }
990 }
991
992 settingsChanged = true;
993 }
994 }
995
996 protected Grabber getGrabber(boolean localOnly) {
997 if (getInfo().getImageryType() == ImageryType.HTML)
998 return new HTMLGrabber(Main.map.mapView, this, localOnly);
999 else if (getInfo().getImageryType() == ImageryType.WMS)
1000 return new WMSGrabber(Main.map.mapView, this, localOnly);
1001 else throw new IllegalStateException("getGrabber() called for non-WMS layer type");
1002 }
1003
1004 public ProjectionBounds getBounds(WMSRequest request) {
1005 ProjectionBounds result = new ProjectionBounds(
1006 getEastNorth(request.getXIndex(), request.getYIndex()),
1007 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
1008
1009 if (WMSLayer.PROP_OVERLAP.get()) {
1010 double eastSize = result.maxEast - result.minEast;
1011 double northSize = result.maxNorth - result.minNorth;
1012
1013 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;
1014 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;
1015
1016 result = new ProjectionBounds(result.getMin(),
1017 new EastNorth(result.maxEast + eastCoef * eastSize,
1018 result.maxNorth + northCoef * northSize));
1019 }
1020 return result;
1021 }
1022
1023 @Override
1024 public boolean isProjectionSupported(Projection proj) {
1025 List<String> serverProjections = info.getServerProjections();
1026 return serverProjections.contains(proj.toCode().toUpperCase())
1027 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84")))
1028 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84"));
1029 }
1030
1031 @Override
1032 public String nameSupportedProjections() {
1033 String res = "";
1034 for(String p : info.getServerProjections()) {
1035 if(!res.isEmpty()) {
1036 res += ", ";
1037 }
1038 res += p;
1039 }
1040 return tr("Supported projections are: {0}", res);
1041 }
1042
1043 @Override
1044 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
1045 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
1046 Main.map.repaint(done ? 0 : 100);
1047 return !done;
1048 }
1049
1050 @Override
1051 public void writeExternal(ObjectOutput out) throws IOException {
1052 out.writeInt(serializeFormatVersion);
1053 out.writeInt(dax);
1054 out.writeInt(day);
1055 out.writeInt(imageSize);
1056 out.writeDouble(info.getPixelPerDegree());
1057 out.writeObject(info.getName());
1058 out.writeObject(info.getExtendedUrl());
1059 out.writeObject(images);
1060 }
1061
1062 @Override
1063 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
1064 int sfv = in.readInt();
1065 if (sfv != serializeFormatVersion)
1066 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion));
1067 autoDownloadEnabled = false;
1068 dax = in.readInt();
1069 day = in.readInt();
1070 imageSize = in.readInt();
1071 info.setPixelPerDegree(in.readDouble());
1072 doSetName((String)in.readObject());
1073 info.setExtendedUrl((String)in.readObject());
1074 images = (GeorefImage[][])in.readObject();
1075
1076 for (GeorefImage[] imgs : images) {
1077 for (GeorefImage img : imgs) {
1078 if (img != null) {
1079 img.setLayer(WMSLayer.this);
1080 }
1081 }
1082 }
1083
1084 settingsChanged = true;
1085 if (Main.isDisplayingMapView()) {
1086 Main.map.mapView.repaint();
1087 }
1088 if (cache != null) {
1089 cache.saveIndex();
1090 cache = null;
1091 }
1092 }
1093
1094 @Override
1095 public void onPostLoadFromFile() {
1096 if (info.getUrl() != null) {
1097 cache = new WmsCache(info.getUrl(), imageSize);
1098 startGrabberThreads();
1099 }
1100 }
1101
1102 @Override
1103 public boolean isSavable() {
1104 return true; // With WMSLayerExporter
1105 }
1106
1107 @Override
1108 public File createAndOpenSaveFileChooser() {
1109 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1110 }
1111}
Note: See TracBrowser for help on using the repository browser.