All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.terrain.HighResolutionTerrain Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.terrain;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.cache.*;
import gov.nasa.worldwind.exception.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.*;
import gov.nasa.worldwind.render.SurfaceQuad;
import gov.nasa.worldwind.util.Logging;

import java.util.*;
import java.util.concurrent.*;

/**
 * Provides operations on the best available terrain. Operations such as line/terrain intersection and surface point
 * computation use the highest resolution terrain data available from the globe's elevation model. Because the best
 * available data may not be available when the operations are performed, the operations block while they retrieve the
 * required data from either the local disk cache or a remote server. A timeout may be specified to limit the amount of
 * time allowed for retrieving data. Operations fail if the timeout is exceeded.
 *
 * @author tag
 * @version $Id: HighResolutionTerrain.java 3420 2015-09-10 23:25:43Z tgaskins $
 */
public class HighResolutionTerrain extends WWObjectImpl implements Terrain
{
    /** Holds a tile's geometry. It's heavyweight so cached when created to enable re-use. */
    protected static class RenderInfo
    {
        protected final int density;
        protected final Vec4 referenceCenter; // all vertices are relative to this point
        protected final float[] vertices;
        protected Position minElevation;
        protected Position maxElevation;

        protected RenderInfo(int density, float[] vertices, Vec4 refCenter, Position minElev, Position maxElev)
        {
            this.density = density;
            this.referenceCenter = refCenter;
            this.vertices = vertices;
            this.minElevation = minElev;
            this.maxElevation = maxElev;
        }

        protected long getSizeInBytes()
        {
            // 2 references, an int, and vertices. (indices are shared among all tiles)
//            System.out.println(2 * 4 + 4 + this.vertices.length * 3 * 4);
            return 2 * 4 + 4 + this.vertices.length * 3 * 4; // 588 bytes at a density of 3
        }
    }

    /**
     * Defines an internal terrain tile. This class is meant to be small and quickly constructed so that many can exist
     * simultaneously and can be created anew for each operation. The geometry they refer to is cached and is
     * independent of a particular RectTile instance. Current and future RectTile instances use the same cached geometry
     * instance.
     */
    protected static class RectTile
    {
        protected final Sector sector;
        protected final int density;
        protected Extent extent; // extent of sector in object coordinates
        protected RenderInfo ri;

        public RectTile(Extent extent, int density, Sector sector)
        {
            this.density = density;
            this.sector = sector;
            this.extent = extent;
        }
    }

    protected static final int DEFAULT_DENSITY = 3;
    protected static final long DEFAULT_CACHE_CAPACITY = (long) 200e6;

    // User-specified fields.
    protected Globe globe;
    protected Sector sector;
    protected double verticalExaggeration = 1;
    protected Long timeout;

    // Internal fields.
    protected int density = DEFAULT_DENSITY;
    protected double targetResolution;
    protected double latTileSize;
    protected double lonTileSize;
    protected int numRows;
    protected int numCols;
    protected MemoryCache geometryCache;
    protected ThreadLocal startTime = new ThreadLocal();

    /**
     * Constructs a terrain object for a specified globe.
     *
     * @param globe            the terrain's globe.
     * @param targetResolution the target terrain resolution, in meters, or null to use the globe's highest resolution.
     *
     * @throws IllegalArgumentException if the globe is null.
     */
    public HighResolutionTerrain(Globe globe, Double targetResolution)
    {
        this(globe, null, targetResolution, null);
    }

    /**
     * Constructs a terrain object for a specified sector of a specified globe.
     *
     * @param globe                the terrain's globe.
     * @param sector               the desired range for the terrain. Only locations within this sector may be used in
     *                             operations. If null, the sector spans the entire globe.
     * @param targetResolution     the target terrain resolution, in meters, or null to use the globe's highest
     *                             resolution.
     * @param verticalExaggeration the vertical exaggeration to apply to terrain elevations. Null indicates no
     *                             exaggeration.
     *
     * @throws IllegalArgumentException if the globe is null.
     */
    public HighResolutionTerrain(Globe globe, Sector sector, Double targetResolution, Double verticalExaggeration)
    {
        if (globe == null)
        {
            String msg = Logging.getMessage("nullValue.GlobeIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        this.globe = globe;
        this.sector = sector != null ? sector : Sector.FULL_SPHERE;

        if (targetResolution != null)
            this.targetResolution = targetResolution / this.globe.getRadius();
        else
            this.targetResolution = this.globe.getElevationModel().getBestResolution(null);

        this.verticalExaggeration = verticalExaggeration != null ? verticalExaggeration : 1;

        this.computeDimensions();

        this.geometryCache = new BasicMemoryCache((long) (0.85 * DEFAULT_CACHE_CAPACITY), DEFAULT_CACHE_CAPACITY);
//        this.geometryCache.addCacheListener(new MemoryCache.CacheListener()
//        {
//            public void entryRemoved(Object key, Object clientObject)
//            {
//                long cap = geometryCache.getCapacity();
//                long cs = geometryCache.getUsedCapacity();
//                long no = geometryCache.getNumObjects();
//                System.out.printf("CACHE CLEAN capacity %d, used %d, num entries %d\n", cap, cs, no);
//            }
//        });
    }

    /**
     * Indicates the proportion of the cache currently used.
     *
     * @return the fraction of the cache currently used: a number between 0 and 1.
     */
    public double getCacheUsage()
    {
        return this.geometryCache.getUsedCapacity() / (double) this.geometryCache.getCapacity();
    }

    /**
     * Returns the number of entries currently in the cache.
     *
     * @return the number of entries currently in the cache.s
     */
    public int getNumCacheEntries()
    {
        return this.geometryCache.getNumObjects();
    }

    /**
     * Returns the object's globe.
     *
     * @return the globe specified to the constructor.
     */
    public Globe getGlobe()
    {
        return globe;
    }

    /**
     * Returns the object's sector.
     *
     * @return the object's sector, either the sector specified at construction or the default sector if no sector was
     * specified at construction.
     */
    public Sector getSector()
    {
        return sector;
    }

    public double getTargetResolution()
    {
        return targetResolution;
    }

    /**
     * Indicates the vertical exaggeration used when performing terrain operations.
     *
     * @return the vertical exaggeration. The default is 1: no exaggeration.
     */
    public double getVerticalExaggeration()
    {
        return this.verticalExaggeration;
    }

    /**
     * Indicates the current timeout for operations requiring terrain data retrieval.
     *
     * @return the current timeout, in milliseconds. May be null.
     *
     * @see #setTimeout(Long)
     */
    public synchronized Long getTimeout()
    {
        return this.timeout;
    }

    /**
     * Specifies the maximum amount of time allowed for retrieval of all terrain data necessary to satisfy an operation.
     * Operations that retrieve data throw a {@link gov.nasa.worldwind.exception.WWTimeoutException} if the specified
     * timeout is exceeded.
     *
     * @param timeout the number of milliseconds to wait. May be null, to indicate that operations have unlimited amount
     *                of time to operate.
     */
    public synchronized void setTimeout(Long timeout)
    {
        this.timeout = timeout;
    }

    public int getDensity()
    {
        return density;
    }

    /**
     * Specifies the number of intervals within a single terrain tile. Density does not affect precision, it just
     * determines how many sample points to include with each internal terrain tile. The precision -- the distance
     * between each sample point -- is governed by the terrain tolerance specified in this object's constructor.
     *
     * @param density the number of intervals used to form a terrain tile.
     */
    public void setDensity(int density)
    {
        this.density = density;
        this.computeDimensions();
    }

    /**
     * Indicates the current cache capacity.
     *
     * @return the current cache capacity, in bytes.
     */
    public long getCacheCapacity()
    {
        return this.geometryCache.getCapacity();
    }

    /**
     * Specifies the cache capacity.
     *
     * @param size the cache capacity, in bytes. Values less than 1 MB are clamped to 1 MB.
     */
    public void setCacheCapacity(long size)
    {
        this.geometryCache.setCapacity(Math.max(size, (long) 1e6));
    }

    /** {@inheritDoc} */
    public Vec4 getSurfacePoint(Position position)
    {
        return this.getSurfacePoint(position.getLatitude(), position.getLongitude(), position.getAltitude());
    }

    /** {@inheritDoc} */
    public Vec4 getSurfacePoint(Angle latitude, Angle longitude, double metersOffset)
    {
        if (latitude == null || longitude == null)
        {
            String msg = Logging.getMessage("nullValue.LatLonIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        try
        {
            this.startTime.set(System.currentTimeMillis());

            RectTile tile = this.getContainingTile(latitude, longitude);

            return tile != null ? this.getSurfacePoint(tile, latitude, longitude, metersOffset) : null;
        }
        catch (InterruptedException e)
        {
            throw new WWRuntimeException(e);
        }
        finally
        {
            this.startTime.set(null); // signals that no operation is active
        }
    }

    /** {@inheritDoc} */
    public Double getElevation(LatLon location)
    {
        if (location == null)
        {
            String msg = Logging.getMessage("nullValue.LatLonIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        Vec4 pt = this.getSurfacePoint(location.getLatitude(), location.getLongitude(), 0);
        if (pt == null)
            return null;

        Vec4 p = this.globe.computePointFromPosition(location.getLatitude(), location.getLongitude(), 0);

        return p.distanceTo3(pt) * (pt.getLength3() >= p.getLength3() ? 1 : -1);
    }

    /**
     * Intersect a line with the terrain.
     * 

* Note: This method produces a result only if the line is below the globe's horizon, i.e., it intersects the * globe's ellipsoid. If the line is above the horizon, null is returned even if there is terrain in the path of the * line. * * @param line the line to intersect * * @return an array of intersections with the terrain, or null if there are no intersections or the line is above * the globe's horizon. * * @deprecated */ public Intersection[] intersect(Line line) { if (line == null) { String msg = Logging.getMessage("nullValue.LineIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } // We need to get two positions to pass to the actual intersection calculator. Make one of those the line's // origin. Make the other the intersection point of the line with the globe's ellipsoid. // Get the position of the line's origin. Position pA = this.globe.computePositionFromPoint(line.getOrigin()); Intersection[] ellipsoidIntersections = this.globe.intersect(line, 0); if (ellipsoidIntersections == null || ellipsoidIntersections.length == 0) return null; Position pB = this.globe.computePositionFromPoint(ellipsoidIntersections[0].getIntersectionPoint()); return this.intersect(pA, pB); } /** {@inheritDoc} */ public Intersection[] intersect(Position pA, Position pB) { if (pA == null || pB == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } try { this.startTime.set(System.currentTimeMillis()); return this.doIntersect(pA, pB); } catch (InterruptedException e) { throw new WWRuntimeException(e); } finally { this.startTime.set(null); // signals that no operation is active } } /** {@inheritDoc} */ public Intersection[] intersect(Position pA, Position pB, int altitudeMode) { if (pA == null || pB == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } // The intersect method expects altitudes to be relative to ground, so make them so if they aren't already. double altitudeA = pA.getAltitude(); double altitudeB = pB.getAltitude(); if (altitudeMode == WorldWind.ABSOLUTE) { altitudeA -= this.getElevation(pA); altitudeB -= this.getElevation(pB); } else if (altitudeMode == WorldWind.CLAMP_TO_GROUND) { altitudeA = 0; altitudeB = 0; } return this.intersect(new Position(pA, altitudeA), new Position(pB, altitudeB)); } /** Defines an interface for returning computed intersections. */ public interface IntersectionCallback { /** * Called with the computed intersections for a line. This method is called only for lines along which * intersections occur. * * @param pA The line's start point. * @param pB The line's end point. * @param intersections An array of intersections. */ void intersection(Position pA, Position pB, Intersection[] intersections); /** * Called if an exception occurs during intersection testing. * * @param exception the exception thrown. */ void exception(Exception exception); } /** * Intersects a specified list of geographic two-position lines with the terrain. * * @param positions The positions to intersect, with the line segments formed by each pair of positions, e.g. the * first line in formed by positions[0] and positions[1], the second by positions[2] and * positions[3], etc. * @param callback An object to call in order to return the computed intersections. * * @throws InterruptedException */ public void intersect(List positions, final IntersectionCallback callback) throws InterruptedException { ExecutorService service = Executors.newFixedThreadPool(10); for (int i = 0; i < positions.size(); i += 2) { final Position pA = positions.get(i); final Position pB = positions.get(i + 1); service.submit(new Runnable() { @Override public void run() { try { Intersection[] intersections = intersect(pA, pB); if (intersections != null) { callback.intersection(pA, pB, intersections); } } catch (Exception e) { callback.exception(e); } } }); } service.shutdown(); service.awaitTermination(100, TimeUnit.DAYS); // wait indefinitely for all threads to complete } /** * Cause the tiles used by subsequent intersection calculations to be cached so that they are available immediately * to those subsequent calculations. *

* Pre-caching is unnecessary and is useful only when it can occur before the intersection calculations are needed. * It will incur extra overhead otherwise. The normal intersection calculations cause the same caching. * * @param pA the line's first position. * @param pB the line's second position. * * @throws IllegalArgumentException if either position is null. * @throws InterruptedException if the operation is interrupted. if the current timeout is exceeded while * retrieving terrain data. */ public void cacheIntersectingTiles(Position pA, Position pB) throws InterruptedException { if (pA == null || pB == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } Line line = this.makeLineFromPositions(pA, pB); if (line == null) return; try { this.startTime.set(System.currentTimeMillis()); List tiles = this.getIntersectingTiles(pA, pB, line); if (tiles == null) return; for (RectTile tile : tiles) { if (tile.ri == null) { this.makeVerts(tile); } } } finally { this.startTime.set(null); // signals that no operation is active } } /** * Cause the tiles used by subsequent intersection calculations to be cached so that they are available immediately * to those subsequent calculations. *

* Pre-caching is unnecessary and is useful only when it can occur before the intersection calculations are needed. * It will incur extra overhead otherwise. The normal intersection calculations cause the same caching. * * @param sector the sector for which to cache elevation data. * * @throws IllegalArgumentException if the specified sector is null. * @throws InterruptedException if the operation is interrupted. if the current timeout is exceeded while * retrieving terrain data. */ public void cacheIntersectingTiles(Sector sector) throws InterruptedException { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } try { this.startTime.set(System.currentTimeMillis()); List tiles = this.getIntersectingTiles(sector); if (tiles == null) return; for (RectTile tile : tiles) { this.makeVerts(tile); } } finally { this.startTime.set(null); // signals that no operation is active } } public List getIntersectionTiles(Position pA, Position pB) throws InterruptedException { Line line = this.makeLineFromPositions(pA, pB); if (line == null) return null; List tiles = this.getIntersectingTiles(pA, pB, line); if (tiles == null || tiles.size() == 0) return null; List sectors = new ArrayList(tiles.size()); for (RectTile tile : tiles) { sectors.add(tile.sector); } return sectors; } /** Computes the row and column dimensions of the tile array. */ protected void computeDimensions() { double resTarget = Math.max(this.globe.getElevationModel().getBestResolution(null), this.targetResolution); this.numCols = (int) Math.ceil(this.sector.getDeltaLonRadians() / (this.density * resTarget)); this.numRows = (int) Math.ceil(this.sector.getDeltaLatRadians() / (this.density * resTarget)); this.lonTileSize = this.sector.getDeltaLonDegrees() / (this.numCols - 1); this.latTileSize = this.sector.getDeltaLatDegrees() / (this.numRows - 1); if (this.geometryCache != null) this.geometryCache.clear(); } /** * Determines the tile that contains a specified location. * * @param latitude the location's latitude. * @param longitude the location's longitude. * * @return the tile containing the specified location. */ protected RectTile getContainingTile(Angle latitude, Angle longitude) { if (!this.sector.contains(latitude, longitude)) return null; int row = this.computeRow(this.sector, latitude); int col = this.computeColumn(this.sector, longitude); return this.createTile(row, col); } /** * Creates a tile for a specified row and column of the tile array. * * @param row the tile's 0-origin row index. * @param col the tile's 0-origin column index. * * @return the tile for the specified row and column, or null if the row or column are invalid. */ protected RectTile createTile(int row, int col) { if (row < 0 || col < 0 || row >= this.numRows || col >= this.numCols) return null; double minLon = Math.max(this.sector.getMinLongitude().degrees + col * this.lonTileSize, -180); double maxLon = Math.min(minLon + this.lonTileSize, 180); double minLat = Math.max(this.sector.getMinLatitude().degrees + row * this.latTileSize, -90); double maxLat = Math.min(minLat + this.latTileSize, 90); return this.createTile(Sector.fromDegrees(minLat, maxLat, minLon, maxLon)); } /** * Creates the tile for a specified sector. * * @param tileSector the sector for which to create the tile. * * @return the tile for the sector, or null if the sector is outside this instance's sector. */ protected RectTile createTile(Sector tileSector) { Extent extent = Sector.computeBoundingBox(this.globe, this.verticalExaggeration, tileSector); return new RectTile(extent, this.density, tileSector); } /** * Computes the row index corresponding to a specified latitude. * * @param range the reference sector, typically that of this terrain instance. * @param latitude the latitude in question. * * @return the row index for the sector. */ protected int computeRow(Sector range, Angle latitude) { double top = range.getMaxLatitude().degrees; double bot = range.getMinLatitude().degrees; double s = (latitude.degrees - bot) / (top - bot); return (int) (s * (double) (this.numRows - 1)); } /** * Computes the column index corresponding to a specified latitude. * * @param range the reference sector, typically that of this terrain instance. * @param longitude the latitude in question. * * @return the column index for the sector. */ protected int computeColumn(Sector range, Angle longitude) { double right = range.getMaxLongitude().degrees; double left = range.getMinLongitude().degrees; double s = (longitude.degrees - left) / (right - left); return (int) (s * (double) (this.numCols - 1)); } protected Line makeLineFromPositions(Position pA, Position pB) throws InterruptedException { if (pA == null || pB == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } RectTile tileA = this.getContainingTile(pA.getLatitude(), pA.getLongitude()); RectTile tileB = this.getContainingTile(pB.getLatitude(), pB.getLongitude()); if (tileA == null || tileB == null) return null; Vec4 ptA = this.getSurfacePoint(tileA, pA.getLatitude(), pA.getLongitude(), pA.getAltitude()); Vec4 ptB = this.getSurfacePoint(tileB, pB.getLatitude(), pB.getLongitude(), pB.getAltitude()); if (ptA == null || ptB == null) return null; if (pA.getLatitude().equals(pB.getLatitude()) && pA.getLongitude().equals(pB.getLongitude()) && pA.getAltitude() == pB.getAltitude()) return null; return new Line(ptA, ptB.subtract3(ptA)); } /** * Computes intersections of a line with the terrain. * * @param pA the line's first position. * @param pB the line's second position. * * @return an array of intersections, or null if no intersections occur. * * @throws InterruptedException if the operation is interrupted. */ protected Intersection[] doIntersect(Position pA, Position pB) throws InterruptedException { Line line = this.makeLineFromPositions(pA, pB); if (line == null) return null; List tiles = this.getIntersectingTiles(pA, pB, line); if (tiles == null) return null; Intersection[] hits; ArrayList list = new ArrayList(); for (RectTile tile : tiles) { if ((hits = this.intersect(tile, line)) != null) list.addAll(Arrays.asList(hits)); } if (list.size() == 0) return null; hits = new Intersection[list.size()]; list.toArray(hits); if (list.size() == 1) return hits; final Vec4 origin = line.getOrigin(); Arrays.sort(hits, new Comparator() { public int compare(Intersection i1, Intersection i2) { if (i1 == null && i2 == null) return 0; if (i2 == null) return -1; if (i1 == null) return 1; Vec4 v1 = i1.getIntersectionPoint(); Vec4 v2 = i2.getIntersectionPoint(); double d1 = origin.distanceTo3(v1); double d2 = origin.distanceTo3(v2); return Double.compare(d1, d2); } }); return hits; } protected List getIntersectingTiles(Sector sector) { int rowA = this.computeRow(this.sector, sector.getMinLatitude()); int colA = this.computeColumn(this.sector, sector.getMinLongitude()); int rowB = this.computeRow(this.sector, sector.getMaxLatitude()); int colB = this.computeColumn(this.sector, sector.getMaxLongitude()); int n = (1 + (rowB - rowA)) * (1 + (colB - colA)); List tiles = new ArrayList(n); for (int col = colA; col <= colB; col++) { for (int row = rowA; row <= rowB; row++) { tiles.add(this.createTile(row, col)); } } return tiles; } /** * Determines and creates the terrain tiles intersected by a specified line. * * @param pA the line's first position. * @param pB the line's second position. * @param line the line to intersect * * @return a list of tiles that likely intersect the line. Some returned tiles may not intersect the line but will * only be near it. */ protected List getIntersectingTiles(Position pA, Position pB, Line line) { // Turn off elevation min/max caching in the elevation model because searching for the intersecting tiles // generates a lot of elevation min/max request that often overflows the elevation model's cache. boolean oldCachingMode = this.getGlobe().getElevationModel().isExtremesCachingEnabled(); this.getGlobe().getElevationModel().setExtremesCachingEnabled(false); try { int rowA = this.computeRow(this.sector, pA.getLatitude()); int colA = this.computeColumn(this.sector, pA.getLongitude()); int rowB = this.computeRow(this.sector, pB.getLatitude()); int colB = this.computeColumn(this.sector, pB.getLongitude()); if (rowB < rowA) { int temp = rowA; rowA = rowB; rowB = temp; } if (colB < colA) { int temp = colA; colA = colB; colB = temp; } List tiles = new ArrayList(); this.doGetIntersectingTiles(rowA, colA, rowB, colB, line, tiles); return tiles.size() > 0 ? tiles : null; } finally { this.getGlobe().getElevationModel().setExtremesCachingEnabled(oldCachingMode); } } protected void doGetIntersectingTiles(int r0, int c0, int r1, int c1, Line line, List tiles) { double minLat = this.sector.getMinLatitude().degrees + r0 * this.latTileSize; double maxLat = this.sector.getMinLatitude().degrees + (r1 + 1) * this.latTileSize; double minLon = this.sector.getMinLongitude().degrees + c0 * this.lonTileSize; double maxLon = this.sector.getMinLongitude().degrees + (c1 + 1) * this.lonTileSize; Extent extent = Sector.computeBoundingBox(this.globe, this.verticalExaggeration, Sector.fromDegrees(minLat, maxLat, minLon, maxLon)); if (!extent.intersects(line)) return; int m = c1 - c0 + 1; int n = r1 - r0 + 1; if (m == 1 && n == 1) { tiles.add(this.createTile(r0, c0)); return; } // Subdivide the tile and recursively test for intersection with the line. Order is SW, SE, NW, NE. When there // is only one column, the SE subdivision is identical to the SW one and need not be tested. When there is // only one row, the NW subdivision is identical to the SW one and need not be tested. In either case (one // column or 1 row) the NE subdivision need not be tested. this.doGetIntersectingTiles(r0, c0, r0 + Math.max(0, n / 2 - 1), c0 + Math.max(0, m / 2 - 1), line, tiles); // SW if (m != 1) this.doGetIntersectingTiles(r0, c0 + m / 2, r0 + Math.max(0, n / 2 - 1), c1, line, tiles); // SE if (n != 1) this.doGetIntersectingTiles(r0 + n / 2, c0, r1, c0 + Math.max(0, m / 2 - 1), line, tiles); // NW if (!(m == 1 || n == 1)) this.doGetIntersectingTiles(r0 + n / 2, c0 + m / 2, r1, c1, line, tiles); // NE } /** * Computes a terrain tile's vertices of draws them from the cache. * * @param tile the tile to compute vertices for * * @throws InterruptedException if the operation is interrupted. * @throws gov.nasa.worldwind.exception.WWTimeoutException if terrain data retrieval exceeds the current timeout. */ protected void makeVerts(RectTile tile) throws InterruptedException { // First see if the vertices have been previously computed and are in the cache. tile.ri = (RenderInfo) this.geometryCache.getObject(tile.sector); if (tile.ri != null) return; tile.ri = this.buildVerts(tile); if (tile.ri != null) { this.geometryCache.add(tile.sector, tile.ri, tile.ri.getSizeInBytes()); } } /** * Computes a terrain tile's vertices. * * @param tile the tile to compute vertices for * * @return the computed vertex information. * * @throws InterruptedException if the operation is interrupted. * @throws gov.nasa.worldwind.exception.WWTimeoutException if terrain data retrieval exceeds the current timeout. */ protected RenderInfo buildVerts(RectTile tile) throws InterruptedException { int density = tile.density; int numVertices = (density + 1) * (density + 1); float[] verts; //Re-use the RenderInfo vertices buffer. If it has not been set or the density has changed, create a new buffer if (tile.ri == null || tile.ri.vertices == null) { verts = new float[numVertices * 3]; } else { verts = tile.ri.vertices; } ArrayList latlons = this.computeLocations(tile); double[] elevations = new double[latlons.size()]; // In general, the best attainable resolution varies over the elevation model, so determine the best // attainable ^for this tile^ and use that as the convergence criteria. double[] localTargetResolution = this.getGlobe().getElevationModel().getBestResolutions(sector); for (int i = 0; i < localTargetResolution.length; i++) { localTargetResolution[i] = Math.max(localTargetResolution[i], this.targetResolution); } this.getElevations(tile.sector, latlons, localTargetResolution, elevations); LatLon centroid = tile.sector.getCentroid(); Vec4 refCenter = globe.computePointFromPosition(centroid.getLatitude(), centroid.getLongitude(), 0d); double minElevation = Double.MAX_VALUE; double maxElevation = -Double.MAX_VALUE; LatLon minElevationLocation = centroid; LatLon maxElevationLocation = centroid; int ie = 0; int iv = 0; Iterator latLonIter = latlons.iterator(); for (int j = 0; j <= density; j++) { for (int i = 0; i <= density; i++) { LatLon latlon = latLonIter.next(); double elevation = this.verticalExaggeration * elevations[ie++]; if (elevation < minElevation) { minElevation = elevation; minElevationLocation = latlon; } if (elevation > maxElevation) { maxElevation = elevation; maxElevationLocation = latlon; } Vec4 p = this.globe.computePointFromPosition(latlon.getLatitude(), latlon.getLongitude(), elevation); verts[iv++] = (float) (p.x - refCenter.x); verts[iv++] = (float) (p.y - refCenter.y); verts[iv++] = (float) (p.z - refCenter.z); } } return new RenderInfo(density, verts, refCenter, new Position(minElevationLocation, minElevation), new Position(maxElevationLocation, maxElevation)); } /** * Indicates whether cached elevations are used exclusively. When this flag is true this high resolution terrain * instance uses {@link ElevationModel#getUnmappedLocalSourceElevation(Angle, Angle)} to retrieve elevations. * This assumes that the highest-resolution elevations for the elevation model are cached locally. */ protected boolean useCachedElevationsOnly = false; /** * Indicates whether cached elevations are used exclusively. When this flag is true this high resolution terrain * instance uses {@link ElevationModel#getUnmappedLocalSourceElevation(Angle, Angle)} to retrieve elevations. * This assumes that the highest-resolution elevations for the elevation model are cached locally. * @param tf true to force caching, otherwise false. The default is false. */ public void setUseCachedElevationsOnly(boolean tf) { this.useCachedElevationsOnly = tf; } /** * Indicates whether cached elevations are used exclusively. When this flag is true this high resolution terrain * instance uses {@link ElevationModel#getUnmappedLocalSourceElevation(Angle, Angle)} to retrieve elevations. * This assumes that the highest-resolution elevations for the elevation model are cached locally. * @return true if cached elevations are forced, otherwise false. */ public boolean isUseCachedElevationsOnly() { return this.useCachedElevationsOnly; } protected void getElevations(Sector sector, List latlons, double[] targetResolution, double[] elevations) throws InterruptedException { if (this.useCachedElevationsOnly) { this.getCachedElevations(latlons, elevations); return; } double[] actualResolution = new double[targetResolution.length]; for (int i = 0; i < targetResolution.length; i++) { actualResolution[i] = Double.MAX_VALUE; } while (!this.resolutionsMeetCriteria(actualResolution, targetResolution)) { actualResolution = this.globe.getElevations(sector, latlons, targetResolution, elevations); if (resolutionsMeetCriteria(actualResolution, targetResolution)) break; // Give the system a chance to retrieve data from the disk cache or the server. Also catches interrupts // and throws interrupt exceptions. Thread.sleep(this.timeout == null ? 5L : Math.min(this.timeout, 5L)); Long timeout = this.getTimeout(); if (this.startTime.get() != null && timeout != null) { if (System.currentTimeMillis() - this.startTime.get() > timeout) throw new WWTimeoutException("Terrain convergence timed out"); } } } protected boolean resolutionsMeetCriteria(double[] actualResolution, double[] targetResolution) { for (int i = 0; i < actualResolution.length; i++) { if (actualResolution[i] > targetResolution[i]) return false; } return true; } protected void getCachedElevations(List latlons, double[] elevations) { ElevationModel em = this.globe.getElevationModel(); for (int i = 0; i < latlons.size(); i++) { LatLon ll = latlons.get(i); double elevation = em.getUnmappedLocalSourceElevation(ll.latitude, ll.longitude); if (elevation == em.getMissingDataSignal()) { elevation = em.getMissingDataReplacement(); } elevations[i] = elevation; } } /** * Computes the tile's cell locations, determined by the tile's density and sector. * * @param tile the tile to compute locations for. * * @return the cell locations. */ protected ArrayList computeLocations(RectTile tile) { int density = tile.density; int numVertices = (density + 1) * (density + 1); Angle latMax = tile.sector.getMaxLatitude(); Angle dLat = tile.sector.getDeltaLat().divide(density); Angle lat = tile.sector.getMinLatitude(); Angle lonMin = tile.sector.getMinLongitude(); Angle lonMax = tile.sector.getMaxLongitude(); Angle dLon = tile.sector.getDeltaLon().divide(density); ArrayList latlons = new ArrayList(numVertices); for (int j = 0; j <= density; j++) { Angle lon = lonMin; for (int i = 0; i <= density; i++) { latlons.add(new LatLon(lat, lon)); if (i == density) lon = lonMax; else lon = lon.add(dLon); if (lon.degrees < -180) lon = Angle.NEG180; else if (lon.degrees > 180) lon = Angle.POS180; } if (j == density) lat = latMax; else lat = lat.add(dLat); } return latlons; } /** * Computes the Cartesian, model-coordinate point of a location within a terrain tile. *

* This operation fails with a {@link gov.nasa.worldwind.exception.WWTimeoutException} if a timeout has been * specified and it is exceeded during the operation. * * @param tile the terrain tile. * @param latitude the location's latitude. * @param longitude the location's longitude. * @param metersOffset the location's distance above the terrain. * * @return the Cartesian, model-coordinate point of a the specified location, or null if the specified location does * not exist within this instance's sector or if the operation is interrupted. * * @throws IllegalArgumentException if the latitude or longitude are null. * @throws gov.nasa.worldwind.exception.WWTimeoutException if the current timeout is exceeded while retrieving * terrain data. * @throws InterruptedException if the operation is interrupted. * @see #setTimeout(Long) */ protected Vec4 getSurfacePoint(RectTile tile, Angle latitude, Angle longitude, double metersOffset) throws InterruptedException { Vec4 result = this.getSurfacePoint(tile, latitude, longitude); if (metersOffset != 0 && result != null) result = applyOffset(result, metersOffset); return result; } /** * Applies a specified vertical offset to a surface point. * * @param point the surface point. * @param metersOffset the vertical offset to add to the point. * * @return a new point offset the specified amount from the input point. */ protected Vec4 applyOffset(Vec4 point, double metersOffset) { Vec4 normal = this.globe.computeSurfaceNormalAtPoint(point); point = Vec4.fromLine3(point, metersOffset, normal); return point; } /** * Computes the Cartesian, model-coordinate point of a location within a terrain tile. *

* This operation fails with a {@link gov.nasa.worldwind.exception.WWTimeoutException} if a timeout has been * specified and it is exceeded during the operation. * * @param tile the terrain tile. * @param latitude the location's latitude. * @param longitude the location's longitude. * * @return the Cartesian, model-coordinate point of a the specified location, or null if the specified location does * not exist within this instance's sector or if the operation is interrupted. * * @throws IllegalArgumentException if the latitude or longitude are null. * @throws gov.nasa.worldwind.exception.WWTimeoutException if the current timeout is exceeded while retrieving * terrain data. * @throws InterruptedException if the operation is interrupted. * @see #setTimeout(Long) */ protected Vec4 getSurfacePoint(RectTile tile, Angle latitude, Angle longitude) throws InterruptedException { if (latitude == null || longitude == null) { String msg = Logging.getMessage("nullValue.LatLonIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (!tile.sector.contains(latitude, longitude)) { // not on this geometry return null; } if (tile.ri == null) this.makeVerts(tile); if (tile.ri == null) return null; double lat = latitude.getDegrees(); double lon = longitude.getDegrees(); double bottom = tile.sector.getMinLatitude().getDegrees(); double top = tile.sector.getMaxLatitude().getDegrees(); double left = tile.sector.getMinLongitude().getDegrees(); double right = tile.sector.getMaxLongitude().getDegrees(); double leftDecimal = (lon - left) / (right - left); double bottomDecimal = (lat - bottom) / (top - bottom); int row = (int) (bottomDecimal * (tile.density)); int column = (int) (leftDecimal * (tile.density)); double l = createPosition(column, leftDecimal, tile.ri.density); double h = createPosition(row, bottomDecimal, tile.ri.density); Vec4 result = interpolate(row, column, l, h, tile.ri); result = result.add3(tile.ri.referenceCenter); return result; } protected static double createPosition(int start, double decimal, int density) { double l = ((double) start) / (double) density; double r = ((double) (start + 1)) / (double) density; return (decimal - l) / (r - l); } protected static Vec4 interpolate(int row, int column, double xDec, double yDec, RenderInfo ri) { int numVerticesPerEdge = ri.density + 1; int bottomLeft = row * numVerticesPerEdge + column; bottomLeft *= 3; int numVertsTimesThree = numVerticesPerEdge * 3; int i = bottomLeft; Vec4 bL = new Vec4(ri.vertices[i++], ri.vertices[i++], ri.vertices[i++]); Vec4 bR = new Vec4(ri.vertices[i++], ri.vertices[i++], ri.vertices[i]); bottomLeft += numVertsTimesThree; i = bottomLeft; Vec4 tL = new Vec4(ri.vertices[i++], ri.vertices[i++], ri.vertices[i++]); Vec4 tR = new Vec4(ri.vertices[i++], ri.vertices[i++], ri.vertices[i]); return interpolate(bL, bR, tR, tL, xDec, yDec); } protected static Vec4 interpolate(Vec4 bL, Vec4 bR, Vec4 tR, Vec4 tL, double xDec, double yDec) { double pos = xDec + yDec; if (pos == 1) { // on the diagonal - what's more, we don't need to do any "oneMinusT" calculation return new Vec4( tL.x * yDec + bR.x * xDec, tL.y * yDec + bR.y * xDec, tL.z * yDec + bR.z * xDec); } else if (pos > 1) { // in the "top right" half // vectors pointing from top right towards the point we want (can be thought of as "negative" vectors) Vec4 horizontalVector = (tL.subtract3(tR)).multiply3(1 - xDec); Vec4 verticalVector = (bR.subtract3(tR)).multiply3(1 - yDec); return tR.add3(horizontalVector).add3(verticalVector); } else { // pos < 1 - in the "bottom left" half // vectors pointing from the bottom left towards the point we want Vec4 horizontalVector = (bR.subtract3(bL)).multiply3(xDec); Vec4 verticalVector = (tL.subtract3(bL)).multiply3(yDec); return bL.add3(horizontalVector).add3(verticalVector); } } /** * Computes the intersections of a line with a tile. * * @param tile the tile. * @param line the line. * * @return an array of intersections, or null if no intersections occur. * * @throws InterruptedException if the operation is interrupted. */ protected Intersection[] intersect(RectTile tile, Line line) throws InterruptedException { if (tile.ri == null) this.makeVerts(tile); if (tile.ri == null) return null; Intersection[] hits; ArrayList list = new ArrayList(); double cx = tile.ri.referenceCenter.x; double cy = tile.ri.referenceCenter.y; double cz = tile.ri.referenceCenter.z; // Loop through all the tile's triangles int n = tile.density + 1; float[] coords = tile.ri.vertices; for (int j = 0; j < n - 1; j++) { for (int i = 0; i < n - 1; i++) { int k = (j * n + i) * 3; Vec4 va = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); k += 3; Vec4 vb = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); k += n * 3; Vec4 vc = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); k -= 3; Vec4 vd = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); // Intersect triangles with line Intersection intersection; if ((intersection = Triangle.intersect(line, va, vb, vc)) != null) list.add(intersection); if ((intersection = Triangle.intersect(line, va, vc, vd)) != null) list.add(intersection); } } int numHits = list.size(); if (numHits == 0) return null; // Sort the intersections by distance from line origin, nearer are first in the sorted list. hits = new Intersection[numHits]; list.toArray(hits); final Vec4 origin = line.getOrigin(); Arrays.sort(hits, new Comparator() { public int compare(Intersection i1, Intersection i2) { if (i1 == null && i2 == null) return 0; if (i2 == null) return -1; if (i1 == null) return 1; Vec4 v1 = i1.getIntersectionPoint(); Vec4 v2 = i2.getIntersectionPoint(); double d1 = origin.distanceTo3(v1); double d2 = origin.distanceTo3(v2); return Double.compare(d1, d2); } }); return hits; } /** * Computes the intersection of a triangle with a terrain tile. * * @param tile the terrain tile * @param triangle the Cartesian coordinates of the triangle. * * @return a list of the intersection points at which the triangle intersects the tile, or null if there are no * intersections. If there are intersections, each entry in the returned list contains a two-element array holding * the Cartesian coordinates of the intersection point with one terrain triangle. In the cases of co-planar * triangles, all three vertices of the terrain triangle are returned, in a three-element array. * * @throws InterruptedException if the operation is interrupted before it completes. */ protected List intersect(RectTile tile, Vec4[] triangle) throws InterruptedException { if (tile.ri == null) this.makeVerts(tile); if (tile.ri == null) return null; ArrayList intersections = new ArrayList(); double cx = tile.ri.referenceCenter.x; double cy = tile.ri.referenceCenter.y; double cz = tile.ri.referenceCenter.z; // Loop through all the tile's triangles int n = tile.density + 1; float[] coords = tile.ri.vertices; Vec4[] triA = new Vec4[3]; Vec4[] triB = new Vec4[3]; Vec4[] iVerts = new Vec4[3]; for (int j = 0; j < n - 1; j++) { for (int i = 0; i < n - 1; i++) { int k = (j * n + i) * 3; triA[0] = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); triB[0] = triA[0]; k += 3; triA[1] = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); k += n * 3; triA[2] = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); triB[1] = triA[2]; k -= 3; triB[2] = new Vec4(coords[k] + cx, coords[k + 1] + cy, coords[k + 2] + cz); // Intersect triangles with input triangle int status = Triangle.intersectTriangles(triangle, triA, iVerts); if (status == 1) { intersections.add(new Vec4[] {iVerts[0], iVerts[1]}); // intersections.add(new Vec4[] {triA[0], triA[1], triA[2], triA[0]}); } else if (status == 0) { intersections.add(new Vec4[] {triA[0], triA[1], triA[2]}); } status = Triangle.intersectTriangles(triangle, triB, iVerts); if (status == 1) { intersections.add(new Vec4[] {iVerts[0], iVerts[1]}); // intersections.add(new Vec4[] {triB[0], triB[1], triB[2], triB[0]}); } else if (status == 0) { intersections.add(new Vec4[] {triB[0], triB[1], triB[2]}); } } } int numHits = intersections.size(); if (numHits == 0) return null; return intersections; } /** * Intersects a specified triangle with the terrain. * * @param triangleCoordinates The Cartesian coordinates of the triangle. * @param trianglePositions The geographic coordinates of the triangle. * @param intersectPositionsOut A list in which to place the intersection positions. May not be null. * * @throws InterruptedException if the operation is interrupted before it completes. */ public void intersectTriangle(Vec4[] triangleCoordinates, Position[] trianglePositions, List intersectPositionsOut) throws InterruptedException { // Get the tiles intersecting the specified sector. Compute the sector from geographic coordinates. Sector sector = Sector.boundingSector(Arrays.asList(trianglePositions)); List tiles = this.getIntersectingTiles(sector); // Eliminate tiles with max altitude below specified min altitude. Determine min altitude using triangle's // geographic coordinates. double minAltitude = trianglePositions[0].getAltitude(); for (int i = 1; i < trianglePositions.length; i++) { if (trianglePositions[i].getAltitude() < minAltitude) minAltitude = trianglePositions[i].getAltitude(); } tiles = this.eliminateLowAltitudeTiles(tiles, minAltitude); // Intersect triangles of remaining tiles with input triangle. List intersections = new ArrayList(); for (RectTile tile : tiles) { List iSects = intersect(tile, triangleCoordinates); if (iSects != null) intersections.addAll(iSects); } // Convert intersection points to positions. this.convertPointsToPositions(intersections, intersectPositionsOut); } protected List eliminateLowAltitudeTiles(List tiles, double minAltitude) throws InterruptedException { List filteredTiles = new ArrayList(); for (RectTile tile : tiles) { if (tile.ri == null) this.makeVerts(tile); if (tile.ri == null) return null; if (tile.ri.maxElevation.getElevation() >= minAltitude) filteredTiles.add(tile); } return filteredTiles; } protected void convertPointsToPositions(List points, List positions) { for (Vec4[] pts : points) { Position[] pos = new Position[pts.length]; for (int i = 0; i < pts.length; i++) { pos[i] = this.getGlobe().computePositionFromPoint(pts[i]); } positions.add(pos); } } /** * Determines the minimum and maximum elevations and their locations within a specified {@link Sector}. * * @param sector The sector in question. * * @return a two-element array containing the minimum and maximum elevations and their locations in the sector. The * minimum as at index 0 in the array, the maximum is at index 1. If either cannot be determined, null is given in * the respective array position. * * @throws InterruptedException if the operation is interrupted before it completes. */ public Position[] getExtremeElevations(Sector sector) throws InterruptedException { // Get the tiles intersecting the specified sector. List tiles = this.getIntersectingTiles(sector); // Find the min and max elevation among the tiles. this.startTime.set(System.currentTimeMillis()); Position[] extremes = new Position[2]; for (RectTile tile : tiles) { if (tile.ri == null) this.makeVerts(tile); if (tile.ri == null) continue; if (extremes[0] == null || tile.ri.minElevation.getElevation() < extremes[0].getElevation()) extremes[0] = tile.ri.minElevation; if (extremes[1] == null || tile.ri.maxElevation.getElevation() > extremes[1].getElevation()) extremes[1] = tile.ri.maxElevation; } return extremes; } /** * Determines the minimum and maximum elevations and their locations within a specified geographic quadrilateral. * * @param center The quadrilateral's center. * @param width The quadrilateral's longitudinal width, in meters. * @param height The quadrilateral's latitudinal height, in meters. * * @return a two-element array containing the minimum and maximum elevations and their locations in the * quadrilateral. The minimum as at index 0 in the array, the maximum is at index 1. If either cannot be determined, * null is given in the respective array position. * * @throws InterruptedException if the operation is interrupted before it completes. */ public Position[] getExtremeElevations(LatLon center, double width, double height) throws InterruptedException { // Compute the quad's geographic corners. SurfaceQuad quad = new SurfaceQuad(center, width, height); Sector sector = Sector.boundingSector(quad.getLocations(this.getGlobe())); // Return the tiles intersecting the specified sector. return this.getExtremeElevations(sector); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy