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

gov.nasa.worldwind.terrain.LocalElevationModel 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.WorldWind;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.data.*;
import gov.nasa.worldwind.exception.WWRuntimeException;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.util.*;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Implements an elevation model for a local file or collection of files containing elevation data.
 * 

* Note: Unless the amount of data associated with the local elevation models is small, it's best to construct and add * elevations to a local elevation model on a thread other than the event dispatch thread in order to avoid freezing the * user interface. * * @author tag * @version $Id: LocalElevationModel.java 2138 2014-07-10 17:31:58Z tgaskins $ */ public class LocalElevationModel extends AbstractElevationModel { /** The min and max elevations. */ protected double[] extremeElevations = null; /** The list of elevation rasters, one per file specified. */ protected CopyOnWriteArrayList tiles = new CopyOnWriteArrayList(); public double getMinElevation() { return this.extremeElevations[0]; } public double getMaxElevation() { return this.extremeElevations[1]; } /** * Returns the sector spanned by this elevation model. * * @return the sector spanned by this elevation model. */ public Sector getSector() { if (this.tiles.size() < 1) return null; Sector sector = null; for (LocalTile tile : this.tiles) { Sector tileSector = tile.sector; sector = sector == null ? tileSector : sector.union(tileSector); } return sector; } public double[] getExtremeElevations(Angle latitude, Angle longitude) { if (latitude == null || longitude == null) { String msg = Logging.getMessage("nullValue.AngleIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (this.tiles.size() == 0) return new double[] {this.getMinElevation(), this.getMaxElevation()}; double min = Double.MAX_VALUE; double max = -min; for (LocalTile tile : this.tiles) { if (tile.sector.contains(latitude, longitude)) { if (tile.minElevation < min) min = tile.minElevation; if (tile.maxElevation > max) max = tile.maxElevation; } } return new double[] { min != Double.MAX_VALUE ? min : this.getMinElevation(), max != -Double.MAX_VALUE ? max : this.getMaxElevation()}; } public double[] getExtremeElevations(Sector sector) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (this.tiles.size() == 0) return new double[] {this.getMinElevation(), this.getMaxElevation()}; double min = Double.MAX_VALUE; double max = -min; for (LocalTile tile : this.tiles) { if (tile.sector.intersects(sector)) { if (tile.minElevation < min) min = tile.minElevation; if (tile.maxElevation > max) max = tile.maxElevation; } } return new double[] { min != Double.MAX_VALUE ? min : this.getMinElevation(), max != -Double.MAX_VALUE ? max : this.getMaxElevation()}; } public double getBestResolution(Sector sector) { double res = Double.MAX_VALUE; for (LocalTile tile : tiles) { if (sector != null && !sector.intersects(tile.sector)) continue; double r = tile.sector.getDeltaLatRadians() / tile.tileHeight; if (r < res) res = r; } return res; } @Override public void setExtremesCachingEnabled(boolean enabled) { } @Override public boolean isExtremesCachingEnabled() { return false; } public double getUnmappedElevation(Angle latitude, Angle longitude) { if (latitude == null || longitude == null) { String msg = Logging.getMessage("nullValue.AngleIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (!this.contains(latitude, longitude)) return this.missingDataFlag; Double e = this.lookupElevation(latitude.radians, longitude.radians); // e will be null if the tile containing the location isn't available, or it will be the computed elevation if // there is one for the location, otherwise it will be the missing data signal. In the latter two cases the // value is returned by this method. If no tile contains the location, this elevation model's minimum elevation // at the location is returned. return e != null ? e : this.getExtremeElevations(latitude, longitude)[0]; } public double getElevations(Sector sector, List latlons, double targetResolution, double[] buffer) { return this.doGetElevations(sector, latlons, targetResolution, buffer, true); } public double getUnmappedElevations(Sector sector, List latlons, double targetResolution, double[] buffer) { return this.doGetElevations(sector, latlons, targetResolution, buffer, false); } /** * Performs the lookup and assembly of elevations for a list of specified locations. This method is provided to * enable subclasses to override this operation. * * @param sector the sector containing the specified locations. * @param latlons the list of locations at which to lookup elevations. * @param targetResolution the desired maximum horizontal resolution of the elevation data to draw from. * @param buffer a buffer in which to return the elevations. Must be at least as large as the list of * locations. * @param mapMissingData indicates whether to replace any elevations that match this elevation model's missing * data signal to this model's missing data replacement value. * * @return the resolution achieved, in radians, or {@link Double#MAX_VALUE} if individual elevations cannot be * determined for all of the locations. * * @throws IllegalArgumentException if either the sector, list of locations or buffer is null, or the buffer size is * not at least as large as the location list. * @see #setMissingDataSignal(double) * @see #setMissingDataReplacement(double) */ @SuppressWarnings( {"UnusedParameters"}) protected double doGetElevations(Sector sector, List latlons, double targetResolution, double[] buffer, boolean mapMissingData) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (latlons == null) { String msg = Logging.getMessage("nullValue.LatLonListIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (buffer == null) { String msg = Logging.getMessage("nullValue.ElevationsBufferIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (buffer.length < latlons.size()) { String msg = Logging.getMessage("ElevationModel.ElevationsBufferTooSmall", latlons.size()); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (this.intersects(sector) == -1) return Double.MAX_VALUE; // as stated in the javadoc above, this is the sentinel for "not in my domain" // Mark the model as used this frame. this.setValue(AVKey.FRAME_TIMESTAMP, System.currentTimeMillis()); for (int i = 0; i < latlons.size(); i++) { LatLon ll = latlons.get(i); if (ll == null) continue; if (!this.contains(ll.getLatitude(), ll.getLongitude())) continue; // If an elevation at the given location is available, write that elevation to the destination buffer. // If an elevation is not available but the location is within the elevation model's coverage, write the // elevation models extreme elevation at the location. Do nothing if the location is not within the // elevation model's coverage. Double e = this.lookupElevation(ll.getLatitude().radians, ll.getLongitude().radians); if (e != null && e != this.missingDataFlag) buffer[i] = e; if (e == null) buffer[i] = this.getExtremeElevations(sector)[0]; else if (mapMissingData && e == this.missingDataFlag) buffer[i] = this.getMissingDataReplacement(); } return this.getBestResolution(sector); } /** * Adds the specified elevation data to this elevation model. * * @param filename the file containing the elevation data. * * @throws IllegalArgumentException if the filename is null or the file does not exist, or if the file does not * contain elevations in a recognizable format. * @throws IOException if an exception occurs while opening or reading the specified file. * @throws WWRuntimeException if an error occurs attempting to read and interpret the elevation data, or if * necessary information is missing from the data file or its auxiliary files. */ public void addElevations(String filename) throws IOException { if (filename == null) { String message = Logging.getMessage("nullValue.FilePathIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.addElevations(new File(filename)); } /** * Adds the specified elevation data to this elevation model. * * @param file the file containing the elevation data. * * @throws IllegalArgumentException if the file is null or does not exist, if the file does not contain elevations * in a recognizable format, or if the specified file does not contain required * information such as the data's location and data type. * @throws IOException if an exception occurs while opening or reading the specified file. * @throws WWRuntimeException if an error occurs attempting to read and interpret the elevation data, or if an * error occurs attempting to read and interpret the file's contents. */ public void addElevations(File file) throws IOException { if (file == null) { String message = Logging.getMessage("nullValue.FileIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (!file.exists()) { String message = Logging.getMessage("generic.FileNotFound", file.getPath()); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // Create a raster reader for the file type. DataRasterReaderFactory readerFactory = (DataRasterReaderFactory) WorldWind.createConfigurationComponent( AVKey.DATA_RASTER_READER_FACTORY_CLASS_NAME); DataRasterReader reader = readerFactory.findReaderFor(file, null); // Before reading the raster, verify that the file contains elevations. AVList metadata = reader.readMetadata(file, null); if (metadata == null || !AVKey.ELEVATION.equals(metadata.getStringValue(AVKey.PIXEL_FORMAT))) { String msg = Logging.getMessage("ElevationModel.SourceNotElevations", file.getAbsolutePath()); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } // Read the file into the raster. DataRaster[] rasters = reader.read(file, null); if (rasters == null || rasters.length == 0) { String msg = Logging.getMessage("ElevationModel.CannotReadElevations", file.getAbsolutePath()); Logging.logger().severe(msg); throw new WWRuntimeException(msg); } for (DataRaster raster : rasters) { this.addRaster(raster, file.getAbsolutePath()); } } /** * Adds a new raster to this elevation model. * * @param raster the raster to add. * @param filename the filename associated with the raster. Used only for error reporting. * * @throws IllegalArgumentException if necessary information is missing from the raster. * @throws IOException if an exception occurs while opening or reading the specified file. * @throws WWRuntimeException if an error occurs attempting to read and interpret the elevation data. */ protected void addRaster(DataRaster raster, String filename) throws IOException { // Determine the sector covered by the elevations. This information is in the GeoTIFF file or auxiliary // files associated with the elevations file. final Sector sector = (Sector) raster.getValue(AVKey.SECTOR); if (sector == null) { String msg = Logging.getMessage("DataRaster.MissingMetadata", AVKey.SECTOR); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } // Request a sub-raster that contains the whole file. This step is necessary because only sub-rasters // are reprojected (if necessary); primary rasters are not. int width = raster.getWidth(); int height = raster.getHeight(); DataRaster subRaster = raster.getSubRaster(width, height, sector, raster); // Verify that the sub-raster can create a ByteBuffer, then create one. if (!(subRaster instanceof ByteBufferRaster)) { String msg = Logging.getMessage("ElevationModel.CannotCreateElevationBuffer", filename); Logging.logger().severe(msg); throw new WWRuntimeException(msg); } ByteBuffer elevations = ((ByteBufferRaster) subRaster).getByteBuffer(); // The sub-raster can now be disposed. Disposal won't affect the ByteBuffer. subRaster.dispose(); this.addElevations(elevations, sector, width, height, raster); // Tne primary raster can now be disposed. raster.dispose(); } /** * Adds new elevations to this elevation model. The elevations are specified as a rectangular array arranged in * row-major order in a linear buffer. * * @param byteBuffer the elevations to add. * @param sector the sector containing the elevations. * @param width the number of elevation values in a row of the array. Each consecutive group of {@code width} * elevation values is considered one row of the array, with the first element of the buffer * starting the row of elevations at the sector's maximum latitude. * @param height the number of rows in the array. * @param parameters a list of key-value pairs indicating information about the raster. A value for {@code * AVKey.DATA_TYPE} is required. Values recognized but not required are {@code * AVKey.MISSING_DATA_SIGNAL}, {@code AVKey.BYTE_ORDER}, {@code AVKey.ELEVATION_MIN} and {@code * AVKey.ELEVATION_MAX}. * * @throws IllegalArgumentException if either the buffer, sector or parameters list is null, or if a key-value pair * for {@code AVKey.DATA_TYPE} is not specified in the parameters. */ public void addElevations(ByteBuffer byteBuffer, Sector sector, int width, int height, AVList parameters) { if (byteBuffer == null) { String message = Logging.getMessage("nullValue.BufferIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (sector == null) { String message = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (parameters == null) { String message = Logging.getMessage("nullValue.ParamsIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // Setup parameters to instruct BufferWrapper on how to interpret the ByteBuffer. AVList bufferParams = new AVListImpl(); bufferParams.setValues(parameters.copy()); Double tileMissingDataFlag = AVListImpl.getDoubleValue(bufferParams, AVKey.MISSING_DATA_SIGNAL); if (tileMissingDataFlag == null) tileMissingDataFlag = this.getMissingDataSignal(); // Check params for ELEVATION_MIN and ELEVATION_MAX and don't have the tile compute them if present. Double minElevation = null; Double maxElevation = null; Object o = bufferParams.getValue(AVKey.ELEVATION_MIN); if (o instanceof Double) minElevation = (Double) o; o = bufferParams.getValue(AVKey.ELEVATION_MAX); if (o instanceof Double) maxElevation = (Double) o; String dataType = bufferParams.getStringValue(AVKey.DATA_TYPE); if (WWUtil.isEmpty(dataType)) { String msg = Logging.getMessage("DataRaster.MissingMetadata", AVKey.DATA_TYPE); Logging.logger().severe(msg); throw new IllegalStateException(msg); } BufferWrapper buffer = BufferWrapper.wrap(byteBuffer, bufferParams); LocalTile tile = new LocalTile(sector, tileMissingDataFlag, width, height, buffer, minElevation, maxElevation); this.tiles.add(tile); this.adjustMinMax(tile); } public int intersects(Sector sector) { boolean intersects = false; for (LocalTile tile : this.tiles) { if (tile.sector.contains(sector)) return 0; if (tile.sector.intersects(sector)) intersects = true; } return intersects ? 1 : -1; } public boolean contains(Angle latitude, Angle longitude) { for (LocalTile tile : this.tiles) { if (tile.sector.contains(latitude, longitude)) return true; } return false; } /** * Updates the min and max elevations for this elevation model to account for a specified tile. * * @param tile the tile to account for. */ protected void adjustMinMax(LocalTile tile) { if (this.extremeElevations == null && tile != null) { this.extremeElevations = new double[] {tile.minElevation, tile.maxElevation}; } else if (tile != null) // adjust for just the input tile { if (tile.minElevation < this.extremeElevations[0]) this.extremeElevations[0] = tile.minElevation; if (tile.maxElevation > this.extremeElevations[1]) this.extremeElevations[1] = tile.maxElevation; } else // Find the min and max among all the tiles { double min = Double.MAX_VALUE; double max = -min; for (LocalTile t : this.tiles) { if (t.minElevation < min) min = t.minElevation; if (t.maxElevation > max) max = t.maxElevation; } this.extremeElevations = new double[] {min != Double.MAX_VALUE ? min : 0, max != -Double.MAX_VALUE ? max : 0}; } } /** * Looks up an elevation for a specified location. * * @param latRadians the latitude of the location, in radians. * @param lonRadians the longitude of the location, in radians. * * @return the elevation if the specified location is contained in this elevation model, null if it's not, or this * elevation model's missing data flag if that's the value at the specified location. */ protected Double lookupElevation(final double latRadians, final double lonRadians) { LocalTile tile = this.findTile(latRadians, lonRadians); if (tile == null) return null; final double sectorDeltaLat = tile.sector.getDeltaLat().radians; final double sectorDeltaLon = tile.sector.getDeltaLon().radians; final double dLat = tile.sector.getMaxLatitude().radians - latRadians; final double dLon = lonRadians - tile.sector.getMinLongitude().radians; final double sLat = dLat / sectorDeltaLat; final double sLon = dLon / sectorDeltaLon; int j = (int) ((tile.tileHeight - 1) * sLat); int i = (int) ((tile.tileWidth - 1) * sLon); int k = j * tile.tileWidth + i; double eLeft = tile.elevations.getDouble(k); double eRight = i < (tile.tileWidth - 1) ? tile.elevations.getDouble(k + 1) : eLeft; // Notice that the below test is against the tile flag, but the value returned is the model's flag. if (tile.isMissingData(eLeft) || tile.isMissingData(eRight)) return this.missingDataFlag; double dw = sectorDeltaLon / (tile.tileWidth - 1); double dh = sectorDeltaLat / (tile.tileHeight - 1); double ssLon = (dLon - i * dw) / dw; double ssLat = (dLat - j * dh) / dh; double eTop = eLeft + ssLon * (eRight - eLeft); if (j < tile.tileHeight - 1 && i < tile.tileWidth - 1) { eLeft = tile.elevations.getDouble(k + tile.tileWidth); eRight = tile.elevations.getDouble(k + tile.tileWidth + 1); // Notice that the below test is against the tile flag, but the value returned is the model's flag. if (tile.isMissingData(eLeft) || tile.isMissingData(eRight)) return this.missingDataFlag; } double eBot = eLeft + ssLon * (eRight - eLeft); return eTop + ssLat * (eBot - eTop); } /** * Finds the tile in this elevation model that contains a specified location. * * @param latRadians the location's latitude, in radians. * @param lonRadians the location's longitude, in radians. * * @return the tile that contains the location, or null if no tile contains it. */ protected LocalTile findTile(final double latRadians, final double lonRadians) { for (LocalTile tile : this.tiles) { if (tile.sector.containsRadians(latRadians, lonRadians)) return tile; } return null; } /** An internal class that represents one elevation raster in the elevation model. */ protected static class LocalTile { /** The sector the tile covers. */ protected final Sector sector; /** The number of elevation values in a row of the raster. */ protected final int tileWidth; /** The number of rows in the raster. */ protected final int tileHeight; /** The minimum elevation contained in this tile's raster. */ protected double minElevation; /** The maximum elevation contained in this tile's raster. */ protected double maxElevation; /** The elevation model's missing data flag. */ protected final double missingDataFlag; /** The elevations. */ protected final BufferWrapper elevations; /** * Constructs a new elevations tile. * * @param sector the sector the tile covers. * @param missingDataFlag the elevation model's missing data flag. * @param tileWidth the number of elevation values in a row of this tile's elevation raster. * @param tileHeight the number of rows in this tile's elevation raster. * @param elevations the elevations. * @param minEl the minimum elevation of this tile. May be null, in which case the minimum is * determined. * @param maxEl the maximum elevation of this tile. May be null, in which case the maximum is * determined. */ protected LocalTile(Sector sector, double missingDataFlag, int tileWidth, int tileHeight, BufferWrapper elevations, Double minEl, Double maxEl) { this.sector = sector; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.missingDataFlag = missingDataFlag; this.elevations = elevations; // One or both of the min/max elevations may have been specified in the metadata. if (minEl != null) this.minElevation = minEl; if (maxEl != null) this.maxElevation = maxEl; if (minEl == null || maxEl == null) this.computeMinMaxElevations(); return; } /** Determines the minimum and maximum elevations of this tile. */ protected void computeMinMaxElevations() { int len = this.elevations.length(); if (len == 0) { this.minElevation = 0; this.maxElevation = 0; return; } double min = Double.MAX_VALUE; double max = -min; for (int i = 0; i < len; i++) { double v = this.elevations.getDouble(i); if (v == this.missingDataFlag) continue; if (v < min) min = v; if (v > max) max = v; } this.minElevation = min; this.maxElevation = max; } /** * Determines whether a value signifies missing data -- voids. This method not only tests the value againsy the * tile's missing data flag, it also tests for its being within range of the tile's minimum and maximum values. * The values also tested for the magic value -32767, which for some reason is commonly used but not always * identified in GeoTiff files. * * @param value the value to test. * * @return true if the value is this tile's missing data signal, or if it is outside the min/max range of the * tile, or if it's equal to -32767. */ protected boolean isMissingData(double value) { return value == this.missingDataFlag || value < this.minElevation || value > this.maxElevation || value == -32767; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy