/******************************************************************************* * 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.earthquakes; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.avlist.AVListImpl; import gov.nasa.worldwind.geom.Position; 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.awt.Color; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipInputStream; 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.render.fastshape.FastShape; import au.gov.ga.earthsci.worldwind.common.util.AVKeyMore; import au.gov.ga.earthsci.worldwind.common.util.HSLColor; import au.gov.ga.earthsci.worldwind.common.util.Loader; import au.gov.ga.earthsci.worldwind.common.util.XMLUtil; /** * A specialised sub-surface layer that reads and displays earthquake data from * a reference data file. * <p/> * Each datapoint in the earthquake file is plotted as a point plotted at the * recorded earthquake depth. * <p/> * Colouring is configurable, and can be based on Date, Magnitude or Depth. * <p/> * This implementation makes use of the {@link FastShape} class to load * earthquake data outside the rendering thread to ensure the interface remains * responsive. * <p/> * Each record in the data file should have the following format (without line * breaks): * * <pre> * double latitude (in degrees) * double longitude (in degrees) * double elevation (in metres - negative indicates subsurface) * double magnitude * long timestamp (in milliseconds since epoc 01 01 1970 00:00:00 UTC) * </pre> * * To save on bandwidth, it is recommended that the data file be compressed into * a .zip file. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class HistoricEarthquakesLayer extends AbstractLayer implements Loader { public final static String DATE_COLORING = "Date"; public final static String MAGNITUDE_COLORING = "Magnitude"; public final static String DEPTH_COLORING = "Depth"; private final static int MAX_DOWNLOAD_ATTEMPTS = 3; private final URL url; private final String coloring; private Long coloringMinDate; private Long coloringMaxDate; private double pointSize; private boolean loaded = false; private int loadAttempts = 0; private boolean loading = false; private final List<LoadingListener> loadingListeners = new ArrayList<LoadingListener>(); private FastShape shape; private final Object shapeLock = new Object(); public HistoricEarthquakesLayer(AVList params) { if (params.getValue(AVKey.URL) == null) { throw new IllegalArgumentException("URL not defined"); } 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; String coloring = MAGNITUDE_COLORING; if (params.getValue(AVKeyMore.COLORING) != null) { coloring = params.getStringValue(AVKeyMore.COLORING); } this.coloring = coloring; pointSize = 1; if (params.getValue(AVKeyMore.POINT_SIZE) != null) { pointSize = (Double) params.getValue(AVKeyMore.POINT_SIZE); } coloringMinDate = (Long) params.getValue(AVKeyMore.COLORING_MIN_DATE); coloringMaxDate = (Long) params.getValue(AVKeyMore.COLORING_MAX_DATE); } public HistoricEarthquakesLayer(Document dom, AVList params) { this(dom.getDocumentElement(), params); } public HistoricEarthquakesLayer(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.checkAndSetStringParam(domElement, params, AVKeyMore.COLORING, "Coloring", xpath); WWXML.checkAndSetDoubleParam(domElement, params, AVKeyMore.POINT_SIZE, "PointSize", xpath); XMLUtil.checkAndSetFormattedDateParam(domElement, params, AVKeyMore.COLORING_MIN_DATE, "ColoringMinDate", xpath); XMLUtil.checkAndSetFormattedDateParam(domElement, params, AVKeyMore.COLORING_MAX_DATE, "ColoringMaxDate", xpath); return params; } @Override protected void doRender(DrawContext dc) { if (!loaded) { loaded = true; loadAttempts++; downloadData(); } synchronized (shapeLock) { if (shape != null) { shape.setPointSize(pointSize); shape.render(dc); } } } 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.getAsInputStream()); } else if (result.getError() != null) { result.getError().printStackTrace(); } } }; Downloader.downloadIfModified(url, handler, handler, true); } }); thread.setDaemon(true); thread.start(); } protected void loadData(InputStream is) { try { boolean isZipFile = url.toExternalForm().toLowerCase().endsWith(".zip"); if (isZipFile) { @SuppressWarnings("resource") //closed by the ObjectInputStream below ZipInputStream zis = new ZipInputStream(is); zis.getNextEntry(); //move to first entry is = zis; } List<Earthquake> quakes = new ArrayList<Earthquake>(); ObjectInputStream ois = new ObjectInputStream(is); try { while (is.available() > 0) { double lat = ois.readDouble(); double lon = ois.readDouble(); double elevation = ois.readDouble(); double magnitude = ois.readDouble(); long timeInMillis = ois.readLong(); Position position = Position.fromDegrees(lat, lon, elevation); Earthquake quake = new Earthquake(position, magnitude, timeInMillis); quakes.add(quake); } } catch (EOFException e) { //When reading from a ZipInputStream, the ObjectInputStream.available() method always returns 0, //so just read it anyway. This will throw an EOFException at some stage (when there's no data //left), this means we are at the end of the file. Ignore it. } finally { ois.close(); } loadEarthquakes(quakes); } catch (IOException e) { e.printStackTrace(); 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 loadEarthquakes(List<Earthquake> earthquakes) { List<Position> positions = new ArrayList<Position>(); for (Earthquake earthquake : earthquakes) { positions.add(earthquake.position); } FloatBuffer colorBuffer = FloatBuffer.allocate(positions.size() * 3); generateColorBuffer(colorBuffer, earthquakes); FastShape shape = new FastShape(positions, GL2.GL_POINTS); shape.setColorBuffer(colorBuffer.array()); shape.setColorBufferElementSize(3); synchronized (shapeLock) { this.shape = shape; } firePropertyChange(AVKey.LAYER, null, this); } private void generateColorBuffer(FloatBuffer colorBuffer, List<Earthquake> earthquakes) { if (DEPTH_COLORING.equalsIgnoreCase(coloring)) { generateDepthColoring(colorBuffer, earthquakes); } else if (DATE_COLORING.equalsIgnoreCase(coloring)) { generateDateColoring(colorBuffer, earthquakes); } else { generateMagnitudeColoring(colorBuffer, earthquakes); } } /** * Populate the color buffer with colours based on earthquake date. * <p/> * Blue (shallow) -> Red (deep) */ protected void generateMagnitudeColoring(FloatBuffer colorBuffer, List<Earthquake> earthquakes) { //magnitude coloring double minMagnitude = Double.MAX_VALUE; double maxMagnitude = -Double.MAX_VALUE; for (Earthquake earthquake : earthquakes) { minMagnitude = Math.min(minMagnitude, earthquake.magnitude); maxMagnitude = Math.max(maxMagnitude, earthquake.magnitude); } for (Earthquake earthquake : earthquakes) { double percent = (earthquake.magnitude - minMagnitude) / (maxMagnitude - minMagnitude); //scale the magnitude (VERY crude equalisation) percent = 1 - Math.pow(percent, 0.2); Color color = new HSLColor((float) (240d * percent), 100f, 50f).getRGB(); colorBuffer.put(color.getRed() / 255f).put(color.getGreen() / 255f).put(color.getBlue() / 255f); } } /** * Populate the color buffer with colours based on earthquake date. * <p/> * Blue (old) -> Red (new) */ protected void generateDateColoring(FloatBuffer colorBuffer, List<Earthquake> earthquakes) { long minTime = Long.MAX_VALUE; long maxTime = Long.MIN_VALUE; //if either of the custom min/max dates are null, calculate from the data if (coloringMinDate == null || coloringMaxDate == null) { for (Earthquake earthquake : earthquakes) { minTime = Math.min(minTime, earthquake.timeInMillis); maxTime = Math.max(maxTime, earthquake.timeInMillis); } } minTime = coloringMinDate != null ? coloringMinDate : minTime; maxTime = coloringMaxDate != null ? coloringMaxDate : maxTime; for (Earthquake earthquake : earthquakes) { double percent = (earthquake.timeInMillis - minTime) / (double) (maxTime - minTime); percent = 1 - Math.max(0, Math.min(1, percent)); Color color = new HSLColor((float) (240d * percent), 100f, 50f).getRGB(); colorBuffer.put(color.getRed() / 255f).put(color.getGreen() / 255f).put(color.getBlue() / 255f); } } /** * Populate the color buffer with colours based on earthquake depth. * <p/> * Blue (shallow) -> Red (deep) */ protected void generateDepthColoring(FloatBuffer colorBuffer, List<Earthquake> earthquakes) { double minElevation = Double.MAX_VALUE; double maxElevation = -Double.MAX_VALUE; for (Earthquake earthquake : earthquakes) { minElevation = Math.min(minElevation, earthquake.position.elevation); maxElevation = Math.max(maxElevation, earthquake.position.elevation); } for (Earthquake earthquake : earthquakes) { double percent = (earthquake.position.elevation - minElevation) / (maxElevation - minElevation); Color color = new HSLColor((float) (240d * percent), 100f, 50f).getRGB(); colorBuffer.put(color.getRed() / 255f).put(color.getGreen() / 255f).put(color.getBlue() / 255f); } } protected static class Earthquake { public final Position position; public final double magnitude; public final long timeInMillis; public Earthquake(Position position, double magnitude, long timeInMillis) { this.position = position; this.magnitude = magnitude; this.timeInMillis = timeInMillis; } } 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); } }