/******************************************************************************* * Copyright 2012 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.worldwind.common.layers.curtain; import static au.gov.ga.earthsci.worldwind.common.util.Util.isEmpty; import gov.nasa.worldwind.Configuration; import gov.nasa.worldwind.WorldWind; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.BasicMemoryCache; import gov.nasa.worldwind.cache.MemoryCache; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Box; import gov.nasa.worldwind.geom.Extent; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.util.TileKey; import java.nio.FloatBuffer; import java.util.Arrays; import java.util.List; import java.util.Map.Entry; import java.util.NavigableMap; import java.util.TreeMap; import com.jogamp.common.nio.Buffers; /** * Defines a path consisting of lat/lon coordinates. Contains functionality for * generating vertex geometry for segments within the path. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class Path { protected final NavigableMap<Double, LatLon> positions = new TreeMap<Double, LatLon>(); protected Angle length; protected static final String CACHE_NAME = "CurtainPath"; protected static final String CACHE_ID = Path.class.getName(); protected long updateFrequency = 2000; // milliseconds protected long exaggerationChangeTime = -1; protected double lastVerticalExaggeration = -Double.MAX_VALUE; public Path(List<LatLon> positions) { if (!WorldWind.getMemoryCacheSet().containsCache(CACHE_ID)) { long size = Configuration.getLongValue(AVKey.SECTOR_GEOMETRY_CACHE_SIZE, 10000000L); MemoryCache cache = new BasicMemoryCache((long) (0.85 * size), size); cache.setName(CACHE_NAME); WorldWind.getMemoryCacheSet().addCache(CACHE_ID, cache); } setPositions(positions); } /* * Private; should only be called by constructor, as vertices are cached. */ private synchronized void setPositions(List<LatLon> positions) { this.positions.clear(); double[] distances = new double[positions.size()]; //last array value is unused, but required for simple second loop //calculate total distance double total = 0d; //in radians for (int i = 0; i < positions.size() - 1; i++) { Angle distance = LatLon.greatCircleDistance(positions.get(i), positions.get(i + 1)); distances[i] = distance.radians; total += distance.radians; } this.length = Angle.fromRadians(total); //calculate percent positions double sum = 0d; for (int i = 0; i < positions.size(); i++) { this.positions.put(sum / total, positions.get(i)); sum += distances[i]; } } /** * @return The length of the path, expressed as an angle. */ public synchronized Angle getLength() { return length; } /** * @return Time between updates to vertices, in milliseconds */ public long getUpdateFrequency() { return updateFrequency; } /** * Set the amount of time between vertex updates, in milliseconds * * @param updateFrequency */ public void setUpdateFrequency(long updateFrequency) { this.updateFrequency = updateFrequency; } /** * @param percent * The percentage expressed as a decimal (e.g. 50% == 0.5) * * @return The {@link LatLon} location that lies <code>percent</code>% of * the way along the path */ public synchronized LatLon getPercentLatLon(double percent) { if (percent <= 0) { return positions.firstEntry().getValue(); } if (percent >= 1) { return positions.lastEntry().getValue(); } if (positions.containsKey(percent)) { return positions.get(percent); } Entry<Double, LatLon> lower = positions.lowerEntry(percent); Entry<Double, LatLon> higher = positions.higherEntry(percent); double p = (percent - lower.getKey()) / (higher.getKey() - lower.getKey()); //TODO add different interpolation methods return LatLon.interpolateGreatCircle(p, lower.getValue(), higher.getValue()); } public synchronized Vec4 getSegmentCenterPoint(DrawContext dc, Segment segment, double top, double bottom, boolean followTerrain) { top *= dc.getVerticalExaggeration(); bottom *= dc.getVerticalExaggeration(); double height = top - bottom; double e = top - segment.getVerticalCenter() * height; LatLon ll = getPercentLatLon(segment.getHorizontalCenter()); if (followTerrain) { e += dc.getGlobe().getElevation(ll.latitude, ll.longitude) * dc.getVerticalExaggeration(); } return dc.getGlobe().computePointFromPosition(ll, e); } public synchronized SegmentGeometry getGeometry(DrawContext dc, CurtainTile tile, double top, double bottom, int subsegments, boolean followTerrain) { if (lastVerticalExaggeration != dc.getVerticalExaggeration()) { exaggerationChangeTime = System.currentTimeMillis(); lastVerticalExaggeration = dc.getVerticalExaggeration(); } MemoryCache cache = WorldWind.getMemoryCache(CACHE_ID); TileKey tileKey = tile.getTileKey(); SegmentGeometry geometry = (SegmentGeometry) cache.getObject(tileKey); if (geometry != null && geometry.getTime() >= System.currentTimeMillis() - this.getUpdateFrequency() && geometry.getTime() >= exaggerationChangeTime) { return geometry; } Segment segment = tile.getSegment(); NavigableMap<Double, LatLon> betweenMap = segmentMap(segment, subsegments); int numVertices = betweenMap.size() * 2; Globe globe = dc.getGlobe(); FloatBuffer verts, texCoords; if (geometry != null) { verts = geometry.getVertices(); texCoords = geometry.getTexCoords(); } else if (dc.getGLRuntimeCapabilities().isUseVertexBufferObject()) { verts = FloatBuffer.allocate(numVertices * 3); texCoords = FloatBuffer.allocate(numVertices * 2); } else { verts = Buffers.newDirectFloatBuffer(numVertices * 3); texCoords = Buffers.newDirectFloatBuffer(numVertices * 2); } Vec4 refCenter = getSegmentCenterPoint(dc, segment, top, bottom, followTerrain); //calculate exaggerated segment top/bottom elevations top *= dc.getVerticalExaggeration(); bottom *= dc.getVerticalExaggeration(); double height = top - bottom; double t = top - segment.getTop() * height; double b = top - segment.getBottom() * height; //ensure t is greater than b (this can occur if exaggeration is 0) if (t <= b) { t = b + 1; } double percentDistance = segment.getHorizontalDelta(); for (Entry<Double, LatLon> entry : betweenMap.entrySet()) { LatLon ll = entry.getValue(); double e = 0; if (followTerrain) { e = globe.getElevation(ll.latitude, ll.longitude) * dc.getVerticalExaggeration(); } Vec4 point1 = globe.computePointFromPosition(ll, t + e); Vec4 point2 = globe.computePointFromPosition(ll, b + e); double percent = (entry.getKey() - segment.getStart()) / percentDistance; verts.put((float) (point1.x - refCenter.x)).put((float) (point1.y - refCenter.y)) .put((float) (point1.z - refCenter.z)); verts.put((float) (point2.x - refCenter.x)).put((float) (point2.y - refCenter.y)) .put((float) (point2.z - refCenter.z)); texCoords.put((float) percent).put(1f); texCoords.put((float) percent).put(0f); } if (geometry == null) { geometry = new SegmentGeometry(dc, verts, texCoords, refCenter); cache.add(tileKey, geometry, geometry.getSizeInBytes()); } else { geometry.update(dc, refCenter); } return geometry; } public synchronized Vec4[] getPointsInSegment(DrawContext dc, Segment segment, double top, double bottom, int subsegments, boolean followTerrain) { //TODO ?? cache value returned from this method, and if called twice with same input parameters, return cached value ?? //TODO create a new function to return some object with a vertex buffer and texture buffer instead of just a Vec4[] array NavigableMap<Double, LatLon> betweenMap = segmentMap(segment, subsegments); Globe globe = dc.getGlobe(); Vec4[] points = new Vec4[betweenMap.size() * 2]; //calculate exaggerated segment top/bottom elevations top *= dc.getVerticalExaggeration(); bottom *= dc.getVerticalExaggeration(); double height = top - bottom; double t = top - segment.getTop() * height; double b = top - segment.getBottom() * height; //add top points, and add bottom points (add them backwards, so it's a loop) int j = 0, k = betweenMap.size() * 2; for (LatLon ll : betweenMap.values()) { double e = 0; if (followTerrain) { // Note: The elevation model has already applied vertical exaggeration in the case of the VerticalExaggerationElevationModel... e = globe.getElevation(ll.latitude, ll.longitude) * dc.getVerticalExaggeration(); } points[j++] = globe.computePointFromPosition(ll, t + e); points[--k] = globe.computePointFromPosition(ll, b + e); } return points; } protected NavigableMap<Double, LatLon> segmentMap(Segment segment, int subsegments) { LatLon start = getPercentLatLon(segment.getStart()); LatLon end = getPercentLatLon(segment.getEnd()); NavigableMap<Double, LatLon> betweenMap = new TreeMap<Double, LatLon>(); //get a sublist of all the points between start and end (non-inclusive) betweenMap.putAll(positions.subMap(segment.getStart(), false, segment.getEnd(), false)); //add the start and end points betweenMap.put(segment.getStart(), start); betweenMap.put(segment.getEnd(), end); //insert any subsegment points for (int i = 0; i < subsegments - 1; i++) { double subsegment = (i + 1) / (double) subsegments; double percent = segment.getStart() + subsegment * segment.getHorizontalDelta(); LatLon pos = getPercentLatLon(percent); betweenMap.put(percent, pos); } return betweenMap; } public synchronized Extent getSegmentExtent(DrawContext dc, Segment segment, double top, double bottom, int subsegments, boolean followTerrain) { Vec4[] points = getPointsInSegment(dc, segment, top, bottom, subsegments, followTerrain); return Box.computeBoundingBox(Arrays.asList(points)); } public synchronized Angle getSegmentLength(Segment segment) { return Angle.fromRadians(getSegmentLengthInRadians(segment)); } public synchronized double getSegmentLengthInRadians(Segment segment) { return segment.getHorizontalDelta() * length.radians; } public synchronized Angle getPercentLength(double percent) { return Angle.fromRadians(getPercentLengthInRadians(percent)); } public synchronized double getPercentLengthInRadians(double percent) { return length.radians * percent; } /** * @return The sector that bounds the path */ public synchronized Sector getBoundingSector() { if (isEmpty(positions)) { return null; } Angle minLat = Angle.fromDegrees(360); Angle minLon = Angle.fromDegrees(360); Angle maxLat = Angle.fromDegrees(-360); Angle maxLon = Angle.fromDegrees(-360); for (LatLon pathPosition : positions.values()) { if (pathPosition.getLatitude().compareTo(minLat) < 0) { minLat = pathPosition.getLatitude(); } if (pathPosition.getLatitude().compareTo(maxLat) > 0) { maxLat = pathPosition.getLatitude(); } if (pathPosition.getLongitude().compareTo(minLon) < 0) { minLon = pathPosition.getLongitude(); } if (pathPosition.getLongitude().compareTo(maxLon) > 0) { maxLon = pathPosition.getLongitude(); } } return new Sector(minLat, maxLat, minLon, maxLon); } }