Worldwind public release 0.2
[worldwind-tracker.git] / gov / nasa / worldwind / BasicElevationModel.java
blob9cbf99395a8f583dd6f26bb682996ffae21aa84e
1 /*
2 Copyright (C) 2001, 2006 United States Government
3 as represented by the Administrator of the
4 National Aeronautics and Space Administration.
5 All Rights Reserved.
6 */
7 package gov.nasa.worldwind;
9 import gov.nasa.worldwind.geom.*;
11 import java.net.*;
12 import java.nio.*;
13 import java.io.*;
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.
34 /**
35 * This class represents a single tile in the data set and contains the information that needs to be cached.
37 * @author Tom Gaskins
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);
62 /**
63 * @param levels
64 * @param minElevation
65 * @param maxElevation
66 * @throws IllegalArgumentException if <code>levels</code> is null or invalid
68 public BasicElevationModel(LevelSet levels, double minElevation, double maxElevation)
70 if (levels == null)
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()
94 return this.levels;
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())
139 return;
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))
160 return;
162 Tile tile = this.elevationModel.createTile(this.tileKey);
163 final java.net.URL url = WorldWind.dataFileCache().findFile(tile.getPath(), false);
164 if (url != null)
166 if (this.elevationModel.loadElevations(tile, url))
168 this.elevationModel.levels.unmarkResourceAbsent(tile);
169 this.elevationModel.firePropertyChange(AVKey.ELEVATION_MODEL, null, this);
170 return;
172 else
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)
187 if (this == o)
188 return true;
189 if (o == null || getClass() != o.getClass())
190 return false;
192 final RequestTask that = (RequestTask) o;
194 //noinspection RedundantIfStatement
195 if (this.tileKey != null ? !this.tileKey.equals(that.tileKey) : that.tileKey != null)
196 return false;
198 return true;
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)
217 return false;
219 if (this.numExpectedValues > 0 && elevations.capacity() != this.numExpectedValues)
220 return false; // corrupt file
222 tile.elevations = elevations;
223 this.addTileToCache(tile, elevations);
225 return true;
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);
233 else
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);
247 else
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);
270 return null;
274 private void downloadElevations(final Tile tile)
276 if (WorldWind.retrievalService().isFull())
277 return;
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);
288 return;
291 URLRetriever retriever = new HTTPRetriever(url, new DownloadPostProcessor(tile, this));
292 if (WorldWind.retrievalService().contains(retriever))
293 return;
295 WorldWind.retrievalService().runRetriever(retriever, 0d);
299 * @param dc
300 * @param sector
301 * @param density
302 * @return
303 * @throws IllegalArgumentException if <code>dc</code> is null, <code>sector</code> is null or <code>density is
304 * negative
306 public final int getTargetResolution(DrawContext dc, Sector sector, int density)
308 if (!this.isEnabled)
309 return 0;
311 if (dc == null)
313 String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
314 WorldWind.logger().log(java.util.logging.Level.FINE, msg);
315 throw new IllegalArgumentException(msg);
317 if (sector == null)
319 String msg = WorldWind.retrieveErrMsg("nullValue.SectorIsNull");
320 WorldWind.logger().log(java.util.logging.Level.FINE, msg);
321 throw new IllegalArgumentException(msg);
323 if (density < 0)
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
347 private Tile tile;
348 private BasicElevationModel elevationModel;
350 public DownloadPostProcessor(Tile tile, BasicElevationModel elevationModel)
352 // don't validate - constructor is only available to classes with private access.
353 this.tile = tile;
354 this.elevationModel = elevationModel;
358 * @param retriever
359 * @return
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))
374 return null;
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);
383 return null;
387 URLRetriever r = (URLRetriever) retriever;
388 ByteBuffer buffer = r.getBuffer();
390 final File outFile = WorldWind.dataFileCache().newFile(tile.getPath());
391 if (outFile == null)
393 String msg = WorldWind.retrieveErrMsg("generic.CantCreateCacheFile")
394 + this.tile.getPath();
395 WorldWind.logger().log(java.util.logging.Level.FINE, msg);
396 return null;
399 if (outFile.exists())
400 return buffer;
402 if (buffer != null)
404 synchronized (elevationModel.fileLock)
406 WWIO.saveBuffer(buffer, outFile);
408 return buffer;
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);
416 finally
418 this.elevationModel.firePropertyChange(AVKey.ELEVATION_MODEL, null, this);
420 return null;
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()
445 return this.sector;
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)
456 return 0;
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);
467 return 0;
469 catch (Exception e)
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);
477 return 0;
483 * @param latitude
484 * @param longitude
485 * @return
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())
491 return 0;
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);
504 if (tile == null)
506 int fallbackRow = tileKey.getRow();
507 int fallbackCol = tileKey.getColumn();
508 for (int fallbackLevelNum = tileKey.getLevelNumber() - 1; fallbackLevelNum >= 0; fallbackLevelNum--)
510 fallbackRow /= 2;
511 fallbackCol /= 2;
512 TileKey fallbackKey = new TileKey(fallbackLevelNum, fallbackRow, fallbackCol,
513 this.levels.getLevel(fallbackLevelNum).getCacheName());
514 tile = this.getTileFromMemory(fallbackKey);
515 if (tile != null)
516 break;
520 if (tile == null)
522 final TileKey zeroKey = new TileKey(latitude, longitude, this.levels.getFirstLevel());
523 this.requestTile(zeroKey);
525 return 0;
528 return this.lookupElevation(latitude.radians, longitude.radians, tile);
532 * @param sector
533 * @param resolution
534 * @return
535 * @throws IllegalArgumentException if <code>sector</code> is null
537 public final Elevations getElevations(Sector sector, int resolution)
539 if (sector == null)
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(
554 resolution));
555 final TileKey keySE = new TileKey(sector.getMinLatitude(), sector.getMaxLongitude(), this.levels.getLevel(
556 resolution));
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);
569 if (tile != null)
571 tiles.add(tile);
572 continue;
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--)
588 fallbackRow /= 2;
589 fallbackCol /= 2;
590 fallbackKey = new TileKey(fallbackLevelNum, fallbackRow, fallbackCol, this.levels.getLevel(
591 fallbackLevelNum).getCacheName());
592 tile = this.getTileFromMemory(fallbackKey);
593 if (tile != null)
595 if (!tiles.contains(tile))
596 tiles.add(tile);
597 break;
599 else
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);
631 else
633 elevations = new BasicElevations(sector, resolution, this);
636 elevations.tiles = tiles;
638 return elevations;
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);