2 Copyright (C) 2001, 2006 United States Government
3 as represented by the Administrator of the
4 National Aeronautics and Space Administration.
7 package gov
.nasa
.worldwind
;
9 import gov
.nasa
.worldwind
.geom
.*;
15 // Implementation notes, not for API doc:
17 // Implements an elevation model based on a quad tree of elevation tiles. Meant to be subclassed by very specific
18 // classes, e.g. Earth/SRTM. A Descriptor passed in at construction gives the configuration parameters. Eventually
19 // Descriptor will be replaced by an XML configuration document.
21 // A "tile" corresponds to one tile of the data set, which has a corresponding unique row/column address in the data
22 // set. An inner class implements Tile. An inner class also implements TileKey, which is used to address the
23 // corresponding Tile in the memory cache.
25 // Clients of this class get elevations from it by first getting an Elevations object for a specific Sector, then
26 // querying that object for the elevation at individual lat/lon positions. The Elevations object captures information
27 // that is used to compute elevations. See in-line comments for a description.
29 // When an elevation tile is needed but is not in memory, a task is threaded off to find it. If it's in the file cache
30 // then it's loaded by the task into the memory cache. If it's not in the file cache then a retrieval is initiated by
31 // the task. The disk is never accessed during a call to getElevations(sector, resolution) because that method is
32 // likely being called when a frame is being rendered. The details of all this are in-line below.
35 * This class represents a single tile in the data set and contains the information that needs to be cached.
38 * @version $Id: BasicElevationModel.java 1732 2007-05-05 07:47:37Z tgaskins $
40 public class BasicElevationModel
extends WWObjectImpl
implements ElevationModel
42 private boolean isEnabled
= true;
43 private final LevelSet levels
;
44 private final double minElevation
;
45 private final double maxElevation
;
46 private long numExpectedValues
= 0;
47 private final Object fileLock
= new Object();
48 private java
.util
.concurrent
.ConcurrentHashMap
<TileKey
, Tile
> levelZeroTiles
=
49 new java
.util
.concurrent
.ConcurrentHashMap
<TileKey
, Tile
>();
50 private gov
.nasa
.worldwind
.MemoryCache memoryCache
= new gov
.nasa
.worldwind
.BasicMemoryCache(8000000, 10000000);
52 private static final class Tile
extends gov
.nasa
.worldwind
.Tile
implements Cacheable
54 private java
.nio
.ShortBuffer elevations
; // the elevations themselves
56 private Tile(Sector sector
, Level level
, int row
, int col
)
58 super(sector
, level
, row
, col
);
66 * @throws IllegalArgumentException if <code>levels</code> is null or invalid
68 public BasicElevationModel(LevelSet levels
, double minElevation
, double maxElevation
)
72 String message
= WorldWind
.retrieveErrMsg("nullValue.LevelSetIsNull");
73 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
);
74 throw new IllegalArgumentException(message
);
77 this.levels
= new LevelSet(levels
); // the caller's levelSet may change internally, so we copy it.
78 this.minElevation
= minElevation
;
79 this.maxElevation
= maxElevation
;
82 public boolean isEnabled()
84 return this.isEnabled
;
87 public void setEnabled(boolean enabled
)
89 this.isEnabled
= enabled
;
92 public LevelSet
getLevels()
97 public final double getMaximumElevation()
99 return this.maxElevation
;
102 public final double getMinimumElevation()
104 return this.minElevation
;
107 public long getNumExpectedValuesPerTile()
109 return numExpectedValues
;
112 public void setNumExpectedValuesPerTile(long numExpectedValues
)
114 this.numExpectedValues
= numExpectedValues
;
117 // Create the tile corresponding to a specified key.
118 private Tile
createTile(TileKey key
)
120 Level level
= this.levels
.getLevel(key
.getLevelNumber());
122 // Compute the tile's SW lat/lon based on its row/col in the level's data set.
123 Angle dLat
= level
.getTileDelta().getLatitude();
124 Angle dLon
= level
.getTileDelta().getLongitude();
126 Angle minLatitude
= Tile
.computeRowLatitude(key
.getRow(), dLat
);
127 Angle minLongitude
= Tile
.computeColumnLongitude(key
.getColumn(), dLon
);
129 Sector tileSector
= new Sector(minLatitude
, minLatitude
.add(dLat
), minLongitude
, minLongitude
.add(dLon
));
131 return new Tile(tileSector
, level
, key
.getRow(), key
.getColumn());
134 // Thread off a task to determine whether the file is local or remote and then retrieve it either from the file
135 // cache or a remote server.
136 private void requestTile(TileKey key
)
138 if (WorldWind
.threadedTaskService().isFull())
141 RequestTask request
= new RequestTask(key
, this);
142 WorldWind
.threadedTaskService().addTask(request
);
145 private static class RequestTask
implements Runnable
147 private final BasicElevationModel elevationModel
;
148 private final TileKey tileKey
;
150 private RequestTask(TileKey tileKey
, BasicElevationModel elevationModel
)
152 this.elevationModel
= elevationModel
;
153 this.tileKey
= tileKey
;
156 public final void run()
158 // check to ensure load is still needed
159 if (this.elevationModel
.areElevationsInMemory(this.tileKey
))
162 Tile tile
= this.elevationModel
.createTile(this.tileKey
);
163 final java
.net
.URL url
= WorldWind
.dataFileCache().findFile(tile
.getPath(), false);
166 if (this.elevationModel
.loadElevations(tile
, url
))
168 this.elevationModel
.levels
.unmarkResourceAbsent(tile
);
169 this.elevationModel
.firePropertyChange(AVKey
.ELEVATION_MODEL
, null, this);
174 // Assume that something's wrong with the file and delete it.
175 gov
.nasa
.worldwind
.WorldWind
.dataFileCache().removeFile(url
);
176 this.elevationModel
.levels
.markResourceAbsent(tile
);
177 String message
= WorldWind
.retrieveErrMsg("generic.DeletedCorruptDataFile") + url
;
178 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
);
182 this.elevationModel
.downloadElevations(tile
);
185 public final boolean equals(Object o
)
189 if (o
== null || getClass() != o
.getClass())
192 final RequestTask that
= (RequestTask
) o
;
194 //noinspection RedundantIfStatement
195 if (this.tileKey
!= null ?
!this.tileKey
.equals(that
.tileKey
) : that
.tileKey
!= null)
201 public final int hashCode()
203 return (this.tileKey
!= null ?
this.tileKey
.hashCode() : 0);
206 public final String
toString()
208 return this.tileKey
.toString();
212 // Reads a tile's elevations from the file cache and adds the tile to the memory cache.
213 private boolean loadElevations(Tile tile
, java
.net
.URL url
)
215 java
.nio
.ShortBuffer elevations
= this.readElevations(url
);
216 if (elevations
== null)
219 if (this.numExpectedValues
> 0 && elevations
.capacity() != this.numExpectedValues
)
220 return false; // corrupt file
222 tile
.elevations
= elevations
;
223 this.addTileToCache(tile
, elevations
);
228 private void addTileToCache(Tile tile
, java
.nio
.ShortBuffer elevations
)
230 // Level 0 tiles are held in the model itself; other levels are placed in the memory cache.
231 if (tile
.getLevelNumber() == 0)
232 this.levelZeroTiles
.putIfAbsent(tile
.getTileKey(), tile
);
234 this.memoryCache
.add(tile
.getTileKey(), tile
, elevations
.limit() * 2);
237 private boolean areElevationsInMemory(TileKey key
)
239 Tile tile
= this.getTileFromMemory(key
);
240 return (tile
!= null && tile
.elevations
!= null);
243 private Tile
getTileFromMemory(TileKey tileKey
)
245 if (tileKey
.getLevelNumber() == 0)
246 return this.levelZeroTiles
.get(tileKey
);
248 return (Tile
) this.memoryCache
.getObject(tileKey
);
251 // Read elevations from the file cache. Don't be confused by the use of a URL here: it's used so that files can
252 // be read using System.getResource(URL), which will draw the data from a jar file in the classpath.
253 // TODO: Look into possibly moving the mapping to a URL into WWIO.
254 private java
.nio
.ShortBuffer
readElevations(java
.net
.URL url
)
258 java
.nio
.ByteBuffer buffer
;
259 synchronized (this.fileLock
)
261 buffer
= gov
.nasa
.worldwind
.WWIO
.readURLContentToBuffer(url
);
263 buffer
.order(java
.nio
.ByteOrder
.LITTLE_ENDIAN
); // TODO: byte order is format dependent
264 return buffer
.asShortBuffer();
266 catch (java
.io
.IOException e
)
268 String message
= WorldWind
.retrieveErrMsg("TiledElevationModel.ExceptionAttemptingToReadTextureFile");
269 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
+ url
);
274 private void downloadElevations(final Tile tile
)
276 if (WorldWind
.retrievalService().isFull())
279 java
.net
.URL url
= null;
282 url
= tile
.getResourceURL();
284 catch (java
.net
.MalformedURLException e
)
286 String message
= WorldWind
.retrieveErrMsg("TiledElevationModel.ExceptionCreatingElevationsUrl");
287 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
+ url
, e
);
291 URLRetriever retriever
= new HTTPRetriever(url
, new DownloadPostProcessor(tile
, this));
292 if (WorldWind
.retrievalService().contains(retriever
))
295 WorldWind
.retrievalService().runRetriever(retriever
, 0d
);
303 * @throws IllegalArgumentException if <code>dc</code> is null, <code>sector</code> is null or <code>density is
306 public final int getTargetResolution(DrawContext dc
, Sector sector
, int density
)
313 String msg
= WorldWind
.retrieveErrMsg("nullValue.DrawContextIsNull");
314 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
315 throw new IllegalArgumentException(msg
);
319 String msg
= WorldWind
.retrieveErrMsg("nullValue.SectorIsNull");
320 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
321 throw new IllegalArgumentException(msg
);
325 String msg
= WorldWind
.retrieveErrMsg("BasicElevationModel.DensityBelowZero");
326 WorldWind
.logger().log(java
.util
.logging
.Level
.FINEST
, msg
);
329 LatLon c
= this.levels
.getSector().getCentroid();
330 double radius
= dc
.getGlobe().getRadiusAt(c
.getLatitude(), c
.getLongitude());
331 double sectorWidth
= sector
.getDeltaLatRadians() * radius
;
332 double targetSize
= 0.8 * sectorWidth
/ (density
); // TODO: make scale of density configurable
334 for (Level level
: this.levels
.getLevels())
336 if (level
.getTexelSize(radius
) < targetSize
)
338 return level
.getLevelNumber();
342 return this.levels
.getNumLevels(); // finest resolution available
345 private static class DownloadPostProcessor
implements RetrievalPostProcessor
348 private BasicElevationModel elevationModel
;
350 public DownloadPostProcessor(Tile tile
, BasicElevationModel elevationModel
)
352 // don't validate - constructor is only available to classes with private access.
354 this.elevationModel
= elevationModel
;
360 * @throws IllegalArgumentException if <code>retriever</code> is null
362 public ByteBuffer
run(Retriever retriever
)
364 if (retriever
== null)
366 String msg
= WorldWind
.retrieveErrMsg("nullValue.RetrieverIsNull");
367 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
368 throw new IllegalArgumentException(msg
);
373 if (!retriever
.getState().equals(Retriever
.RETRIEVER_STATE_SUCCESSFUL
))
376 if (retriever
instanceof HTTPRetriever
)
378 HTTPRetriever htr
= (HTTPRetriever
) retriever
;
379 if (htr
.getResponseCode() != HttpURLConnection
.HTTP_OK
)
381 // Mark tile as missing so avoid excessive attempts
382 this.elevationModel
.levels
.markResourceAbsent(this.tile
);
387 URLRetriever r
= (URLRetriever
) retriever
;
388 ByteBuffer buffer
= r
.getBuffer();
390 final File outFile
= WorldWind
.dataFileCache().newFile(tile
.getPath());
393 String msg
= WorldWind
.retrieveErrMsg("generic.CantCreateCacheFile")
394 + this.tile
.getPath();
395 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
399 if (outFile
.exists())
404 synchronized (elevationModel
.fileLock
)
406 WWIO
.saveBuffer(buffer
, outFile
);
411 catch (java
.io
.IOException e
)
413 String message
= WorldWind
.retrieveErrMsg("TiledElevationModel.ExceptionSavingRetrievedElevationFile");
414 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
+ tile
.getPath(), e
);
418 this.elevationModel
.firePropertyChange(AVKey
.ELEVATION_MODEL
, null, this);
424 private static class BasicElevations
implements ElevationModel
.Elevations
426 private final int resolution
;
427 private final Sector sector
;
428 private final BasicElevationModel elevationModel
;
429 private java
.util
.Set
<Tile
> tiles
;
431 private BasicElevations(Sector sector
, int resolution
, BasicElevationModel elevationModel
)
433 this.sector
= sector
;
434 this.resolution
= resolution
;
435 this.elevationModel
= elevationModel
;
438 public int getResolution()
440 return this.resolution
;
443 public Sector
getSector()
448 public boolean hasElevations()
450 return this.tiles
!= null && this.tiles
.size() > 0;
453 public double getElevation(double latRadians
, double lonRadians
)
455 if (this.tiles
== null)
460 // TODO: Tiles are sorted by level/row/column. Use that to find containing sector faster.
461 for (BasicElevationModel
.Tile tile
: this.tiles
)
463 if (tile
.getSector().containsRadians(latRadians
, lonRadians
))
464 return this.elevationModel
.lookupElevation(latRadians
, lonRadians
, tile
);
471 // Throwing an exception within what's likely to be the caller's geometry creation loop
472 // would be hard to recover from, and a reasonable response to the exception can be done here.
473 String message
= WorldWind
.retrieveErrMsg("BasicElevationModel.ExceptionComputingElevation");
474 message
+= "(" + latRadians
+ ", " + lonRadians
+ ")";
475 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, message
, e
);
486 * @throws IllegalArgumentException if <code>latitude</code> or <code>longitude</code> is null
488 public final double getElevation(Angle latitude
, Angle longitude
)
490 if (!this.isEnabled())
493 if (latitude
== null || longitude
== null)
495 String msg
= WorldWind
.retrieveErrMsg("nullValue.AngleIsNull");
496 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
497 throw new IllegalArgumentException(msg
);
500 // TODO: Make level to draw elevations from configurable
501 final TileKey tileKey
= new TileKey(latitude
, longitude
, this.levels
.getLastLevel());
502 Tile tile
= this.getTileFromMemory(tileKey
);
506 int fallbackRow
= tileKey
.getRow();
507 int fallbackCol
= tileKey
.getColumn();
508 for (int fallbackLevelNum
= tileKey
.getLevelNumber() - 1; fallbackLevelNum
>= 0; fallbackLevelNum
--)
512 TileKey fallbackKey
= new TileKey(fallbackLevelNum
, fallbackRow
, fallbackCol
,
513 this.levels
.getLevel(fallbackLevelNum
).getCacheName());
514 tile
= this.getTileFromMemory(fallbackKey
);
522 final TileKey zeroKey
= new TileKey(latitude
, longitude
, this.levels
.getFirstLevel());
523 this.requestTile(zeroKey
);
528 return this.lookupElevation(latitude
.radians
, longitude
.radians
, tile
);
535 * @throws IllegalArgumentException if <code>sector</code> is null
537 public final Elevations
getElevations(Sector sector
, int resolution
)
541 String msg
= WorldWind
.retrieveErrMsg("nullValue.SectorIsNull");
542 WorldWind
.logger().log(java
.util
.logging
.Level
.FINE
, msg
);
543 throw new IllegalArgumentException(msg
);
546 if (!this.isEnabled())
547 return new BasicElevations(sector
, Integer
.MIN_VALUE
, this);
549 // Collect all the elevation tiles intersecting the input sector. If a desired tile is not curently
550 // available, choose its next lowest resolution parent that is available.
551 final Level targetLevel
= this.levels
.getLevel(resolution
);
553 final TileKey keyNW
= new TileKey(sector
.getMaxLatitude(), sector
.getMinLongitude(), this.levels
.getLevel(
555 final TileKey keySE
= new TileKey(sector
.getMinLatitude(), sector
.getMaxLongitude(), this.levels
.getLevel(
558 java
.util
.TreeSet
<Tile
> tiles
= new java
.util
.TreeSet
<Tile
>();
559 java
.util
.ArrayList
<TileKey
> requested
= new java
.util
.ArrayList
<TileKey
>();
561 boolean missingTargetTiles
= false;
562 boolean missingLevelZeroTiles
= false;
563 for (int row
= keySE
.getRow(); row
<= keyNW
.getRow(); row
++)
565 for (int col
= keyNW
.getColumn(); col
<= keySE
.getColumn(); col
++)
567 TileKey key
= new TileKey(resolution
, row
, col
, targetLevel
.getCacheName());
568 Tile tile
= this.getTileFromMemory(key
);
575 missingTargetTiles
= true;
576 this.requestTile(key
);
578 // Determine the fallback to use. Simultaneously determine a fallback to request that is
579 // the next resolution higher than the fallback chosen, if any. This will progressively
580 // refine the display until the desired resolution tile arrives.
581 TileKey fallbackToRequest
= null;
582 TileKey fallbackKey
= null;
584 int fallbackRow
= row
;
585 int fallbackCol
= col
;
586 for (int fallbackLevelNum
= key
.getLevelNumber() - 1; fallbackLevelNum
>= 0; fallbackLevelNum
--)
590 fallbackKey
= new TileKey(fallbackLevelNum
, fallbackRow
, fallbackCol
, this.levels
.getLevel(
591 fallbackLevelNum
).getCacheName());
592 tile
= this.getTileFromMemory(fallbackKey
);
595 if (!tiles
.contains(tile
))
601 if (fallbackLevelNum
== 0)
602 missingLevelZeroTiles
= true;
603 fallbackToRequest
= fallbackKey
; // keep track of lowest level to request
607 if (fallbackToRequest
!= null)
609 if (!requested
.contains(fallbackKey
))
611 this.requestTile(fallbackKey
);
612 requested
.add(fallbackKey
); // keep track to avoid overhead of duplicte requests
618 BasicElevations elevations
;
620 if (missingLevelZeroTiles
|| tiles
.isEmpty())
622 // Integer.MIN_VALUE is a signal for no in-memory tile for a given region of the sector.
623 elevations
= new BasicElevations(sector
, Integer
.MIN_VALUE
, this);
625 else if (missingTargetTiles
)
627 // Use the level of the the lowest resolution found as the resolution for this elevation set.
628 // The list of tiles is sorted first by level, so use the level of the list's first entry.
629 elevations
= new BasicElevations(sector
, tiles
.first().getLevelNumber(), this);
633 elevations
= new BasicElevations(sector
, resolution
, this);
636 elevations
.tiles
= tiles
;
641 private double lookupElevation(final double latRadians
, final double lonRadians
, final Tile tile
)
643 Sector sector
= tile
.getSector();
644 final int tileHeight
= tile
.getLevel().getTileHeight();
645 final int tileWidth
= tile
.getLevel().getTileWidth();
646 final double sectorDeltaLat
= sector
.getDeltaLat().radians
;
647 final double sectorDeltaLon
= sector
.getDeltaLon().radians
;
648 final double dLat
= sector
.getMaxLatitude().radians
- latRadians
;
649 final double dLon
= lonRadians
- sector
.getMinLongitude().radians
;
650 final double sLat
= dLat
/ sectorDeltaLat
;
651 final double sLon
= dLon
/ sectorDeltaLon
;
653 int j
= (int) ((tileHeight
- 1) * sLat
);
654 int i
= (int) ((tileWidth
- 1) * sLon
);
655 int k
= j
* tileWidth
+ i
;
657 double eLeft
= tile
.elevations
.get(k
);
658 double eRight
= i
< (tileWidth
- 1) ? tile
.elevations
.get(k
+ 1) : eLeft
;
660 double dw
= sectorDeltaLon
/ (tileWidth
- 1);
661 double dh
= sectorDeltaLat
/ (tileHeight
- 1);
662 double ssLon
= (dLon
- i
* dw
) / dw
;
663 double ssLat
= (dLat
- j
* dh
) / dh
;
665 double eTop
= eLeft
+ ssLon
* (eRight
- eLeft
);
667 if (j
< tileHeight
- 1 && i
< tileWidth
- 1)
669 eLeft
= tile
.elevations
.get(k
+ tileWidth
);
670 eRight
= tile
.elevations
.get(k
+ tileWidth
+ 1);
673 double eBot
= eLeft
+ ssLon
* (eRight
- eLeft
);
674 return eTop
+ ssLat
* (eBot
- eTop
);