/*******************************************************************************
* 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.crust;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.avlist.AVList;
import gov.nasa.worldwind.avlist.AVListImpl;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Sector;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.AbstractLayer;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.util.Logging;
import gov.nasa.worldwind.util.WWXML;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.DoubleBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.media.opengl.GL2;
import javax.xml.xpath.XPath;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import au.gov.ga.earthsci.worldwind.common.downloader.Downloader;
import au.gov.ga.earthsci.worldwind.common.downloader.RetrievalHandler;
import au.gov.ga.earthsci.worldwind.common.downloader.RetrievalResult;
import au.gov.ga.earthsci.worldwind.common.util.AVKeyMore;
import au.gov.ga.earthsci.worldwind.common.util.Loader;
import com.jogamp.common.nio.Buffers;
/**
* A specialised sub-surface layer that displays crustal elevation data read
* from a simple comma- or whitespace-separated data file.
* <p/>
* The data file (referenced via the {@link #url} field) should contain
* elevations expressed as doubles (in metres) in a row-major ordering of
* dimensions {@link #width} x {@link #height}. The datafile can be contaied
* within a zip file to minimise bandwidth requirements.
* <p/>
* The crust layer will be rendered as a surface deformed by the elevation data
* and coloured using a colour map based on min and max elevation values.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class CrustLayer extends AbstractLayer implements Loader
{
private static final String WHITESPACE_COMMA_REGEX = "(\\s*,\\s*)|\\s+";
private final static int MAX_DOWNLOAD_ATTEMPTS = 3;
private final URL url;
private final int width;
private final int height;
private final double scale;
private final Sector sector;
private boolean loaded = false;
private int loadAttempts = 0;
private boolean loading = false;
private final List<LoadingListener> loadingListeners = new ArrayList<LoadingListener>();
private final Object elevationLock = new Object();
private DoubleBuffer elevations;
private DoubleBuffer vertices;
private DoubleBuffer colors;
private IntBuffer indices;
private double minElevation = Double.MAX_VALUE;
private double maxElevation = -Double.MAX_VALUE;
private double lastVerticalExaggeration = -1;
private Globe lastGlobe = null;
public CrustLayer(AVList params)
{
URL url = null;
try
{
URL context = (URL) params.getValue(AVKeyMore.CONTEXT_URL);
url = new URL(context, params.getStringValue(AVKey.URL));
}
catch (MalformedURLException e)
{
throw new IllegalArgumentException(e);
}
this.url = url;
this.width = (Integer) params.getValue(AVKey.WIDTH);
this.height = (Integer) params.getValue(AVKey.HEIGHT);
this.sector = (Sector) params.getValue(AVKey.SECTOR);
if (width <= 1 || height <= 1)
{
throw new IllegalArgumentException("Illegal width or height");
}
double scale = 1;
if (params.getValue(AVKeyMore.SCALE) != null)
{
scale = (Double) params.getValue(AVKeyMore.SCALE);
}
this.scale = scale;
boolean wrap = false;
if (params.getValue(AVKeyMore.WRAP) != null)
{
wrap = (Boolean) params.getValue(AVKeyMore.WRAP);
}
indices = generateTriStripIndices(width, height, wrap);
vertices = Buffers.newDirectDoubleBuffer(width * height * 3);
colors = Buffers.newDirectDoubleBuffer(width * height * 4);
}
public CrustLayer(Document dom, AVList params)
{
this(dom.getDocumentElement(), params);
}
public CrustLayer(Element domElement, AVList params)
{
this(getParamsFromDocument(domElement, params));
}
protected static AVList getParamsFromDocument(Element domElement, AVList params)
{
if (params == null)
{
params = new AVListImpl();
}
XPath xpath = WWXML.makeXPath();
// Common layer properties.
AbstractLayer.getLayerConfigParams(domElement, params);
WWXML.checkAndSetStringParam(domElement, params, AVKey.URL, "URL", xpath);
WWXML.checkAndSetIntegerParam(domElement, params, AVKey.WIDTH, "Size/@width", xpath);
WWXML.checkAndSetIntegerParam(domElement, params, AVKey.HEIGHT, "Size/@height", xpath);
WWXML.checkAndSetDoubleParam(domElement, params, AVKeyMore.SCALE, "Scale", xpath);
WWXML.checkAndSetSectorParam(domElement, params, AVKey.SECTOR, "Sector", xpath);
WWXML.checkAndSetBooleanParam(domElement, params, AVKeyMore.WRAP, "Wrap", xpath);
return params;
}
private void recalculateVertices(Globe globe, double verticalExaggeration)
{
synchronized (elevationLock)
{
if (elevations != null)
{
elevations.rewind();
vertices.rewind();
Angle minlon = sector.getMinLongitude();
Angle minlat = sector.getMaxLatitude();
double lonstep = sector.getDeltaLonDegrees() / (width - 1);
double latstep = sector.getDeltaLatDegrees() / (height - 1);
for (int y = 0; y < height; y++)
{
Angle lat = minlat.subtractDegrees(latstep * y);
for (int x = 0; x < width; x++)
{
Angle lon = minlon.addDegrees(lonstep * x);
double elev = elevations.get() * scale * verticalExaggeration;
Vec4 point = globe.computePointFromPosition(lat, lon, elev);
vertices.put(point.x).put(point.y).put(point.z);
}
}
}
}
}
private void recalculateColors()
{
synchronized (elevationLock)
{
if (elevations != null)
{
elevations.rewind();
colors.rewind();
for (int i = 0; i < width * height; i++)
{
double[] color =
chroma((elevations.get() - minElevation) / (maxElevation - minElevation), getOpacity());
colors.put(color);
}
}
}
}
@Override
public void setOpacity(double opacity)
{
super.setOpacity(opacity);
recalculateColors();
}
protected static IntBuffer generateTriStripIndices(int width, int height, boolean wrapWidth)
{
int w = width - 1;
if (!wrapWidth)
{
width--;
}
height--;
int indexCount = 2 * width * height + 4 * width - 2;
IntBuffer buffer = Buffers.newDirectIntBuffer(indexCount);
int k = 0;
for (int i = 0; i < width; i++)
{
buffer.put(k);
if (i > 0)
{
buffer.put(++k);
buffer.put(k);
}
if (i % 2 == 0) // even
{
buffer.put(++k);
for (int j = 0; j < height; j++)
{
k += w;
buffer.put(k);
buffer.put(++k);
}
}
else
// odd
{
buffer.put(--k);
for (int j = 0; j < height; j++)
{
k -= w;
buffer.put(k);
buffer.put(--k);
}
}
}
if (wrapWidth)
{
boolean even = width % 2 == 0;
int fixLast = 2 * height + 3 - (even ? 0 : 1);
for (int i = indexCount - fixLast + 1; i < indexCount; i += 2)
{
buffer.put(i, buffer.get(i) - width);
}
if (even)
{
buffer.put(fixLast, buffer.get(fixLast) - width);
}
}
return buffer;
}
@Override
protected void doRender(DrawContext dc)
{
if (!loaded)
{
loaded = true;
loadAttempts++;
downloadData();
}
if (lastVerticalExaggeration != dc.getVerticalExaggeration() || lastGlobe != dc.getGlobe())
{
lastVerticalExaggeration = dc.getVerticalExaggeration();
lastGlobe = dc.getGlobe();
recalculateVertices(lastGlobe, lastVerticalExaggeration);
recalculateColors();
}
GL2 gl = dc.getGL().getGL2();
int push = GL2.GL_CLIENT_VERTEX_ARRAY_BIT;
if (colors != null)
{
push |= GL2.GL_COLOR_BUFFER_BIT;
}
if (getOpacity() < 1.0)
{
push |= GL2.GL_CURRENT_BIT;
}
gl.glPushClientAttrib(push);
if (colors != null)
{
gl.glEnableClientState(GL2.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL2.GL_DOUBLE, 0, colors.rewind());
}
if (getOpacity() < 1.0)
{
setBlendingFunction(dc);
}
gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL2.GL_DOUBLE, 0, vertices.rewind());
gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, indices.limit(), GL2.GL_UNSIGNED_INT, indices.rewind());
gl.glColor4d(1, 1, 1, 1);
gl.glPopClientAttrib();
}
protected void downloadData()
{
//run download in separate thread, so that data loading from download
//cache doesn't freeze up the render thread
Thread thread = new Thread(new Runnable()
{
@Override
public void run()
{
RetrievalHandler handler = new RetrievalHandler()
{
@Override
public void handle(RetrievalResult result)
{
if (result.hasData())
{
loadData(result.getAsString());
}
else if (result.getError() != null)
{
result.getError().printStackTrace();
}
}
};
Downloader.downloadIfModified(url, handler, handler, true);
}
});
thread.setDaemon(true);
thread.start();
}
protected void loadData(String s)
{
try
{
double[] doubles = parseDoubles(s);
if (doubles.length != width * height)
{
throw new IOException("File doesn't contain width x height (" + (width * height) + ") values");
}
DoubleBuffer buffer = Buffers.newDirectDoubleBuffer(width * height);
buffer.put(doubles);
buffer.rewind();
for (int i = 0; i < width * height; i++)
{
double elev = buffer.get();
minElevation = Math.min(minElevation, elev);
maxElevation = Math.max(maxElevation, elev);
}
synchronized (elevationLock)
{
this.elevations = buffer;
}
//force a recalculate
lastGlobe = null;
firePropertyChange(AVKey.LAYER, null, this);
}
catch (IOException e)
{
if (loadAttempts < MAX_DOWNLOAD_ATTEMPTS)
{
loaded = false;
Downloader.removeCache(url);
Logging.logger().warning("Deleted corrupt cached data file for " + url);
}
else
{
e.printStackTrace();
}
}
}
protected void setBlendingFunction(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2();
double alpha = this.getOpacity();
gl.glColor4d(alpha, alpha, alpha, alpha);
gl.glEnable(GL2.GL_BLEND);
gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA);
}
private static double[] chroma(double depth, double opacity)
{
double r = 2.0 - depth * 4.0;
double b = depth * 4.0 - 2.0;
double g = depth * 4.0;
if (g >= 2.0)
{
g = 4.0 - g;
}
return new double[] { clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1), opacity };
}
private static double clamp(double value, double min, double max)
{
return value > max ? max : value < min ? min : value;
}
private static double[] parseDoubles(String string)
{
String[] split = string.trim().split(WHITESPACE_COMMA_REGEX);
double[] array = new double[split.length];
try
{
for (int i = 0; i < array.length; i++)
{
array[i] = Double.parseDouble(split[i]);
}
}
catch (NumberFormatException e)
{
e.printStackTrace();
}
return array;
}
protected void fireLoadingStateChanged()
{
for (int i = loadingListeners.size() - 1; i >= 0; i--)
{
loadingListeners.get(i).loadingStateChanged(this, isLoading());
}
}
protected void setLoading(boolean loading)
{
this.loading = loading;
}
@Override
public boolean isLoading()
{
return loading;
}
@Override
public void addLoadingListener(LoadingListener listener)
{
loadingListeners.remove(listener);
}
@Override
public void removeLoadingListener(LoadingListener listener)
{
loadingListeners.add(listener);
}
}