/******************************************************************************* * 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.event.SelectEvent; import gov.nasa.worldwind.event.SelectListener; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.layers.RenderableLayer; import gov.nasa.worldwind.render.AnnotationAttributes; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.render.FrameFactory; import gov.nasa.worldwind.render.GlobeAnnotation; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.OGLStackHandler; import gov.nasa.worldwind.util.WWXML; import gov.nasa.worldwind.view.BasicView; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.StringReader; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; import java.nio.DoubleBuffer; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.media.opengl.GL2; import javax.swing.Timer; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import au.gov.ga.earthsci.worldwind.common.WorldWindowRegistry; 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.DefaultLauncher; 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.MapBackedNamespaceContext; /** * A {@link RenderableLayer} that displays recent earthquake data sourced from a * RSS feed. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class RSSEarthquakesLayer extends RenderableLayer implements Loader, SelectListener { private static final String RSS_URL = "http://www.ga.gov.au/earthquakes/all_recent.rss"; private static final int UPDATE_TIME = 10 * 60 * 1000; //10 minutes private static final long ONE_DAY = 24 * 60 * 60 * 1000; //1 day private static final long MAX_TIME = 30 * ONE_DAY; private AnnotationAttributes attributes; private Timer updateTimer; private SurfaceEarthquakeAnnotation mouseEq, latestEq; private GlobeAnnotation tooltipAnnotation; private List<LoadingListener> loadingListeners = new ArrayList<LoadingListener>(); private boolean loading; public RSSEarthquakesLayer() { // Init tooltip annotation this.tooltipAnnotation = new GlobeAnnotation("", Position.fromDegrees(0, 0, 0)); Font font = Font.decode("Arial-Plain-16"); this.tooltipAnnotation.getAttributes().setFont(font); this.tooltipAnnotation.getAttributes().setSize(new Dimension(270, 0)); this.tooltipAnnotation.getAttributes().setDistanceMinScale(1); this.tooltipAnnotation.getAttributes().setDistanceMaxScale(1); this.tooltipAnnotation.getAttributes().setVisible(false); this.tooltipAnnotation.setPickEnabled(false); this.tooltipAnnotation.setAlwaysOnTop(true); updateTimer = new Timer(UPDATE_TIME, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { startEarthquakeDownload(); } }); updateTimer.start(); startEarthquakeDownload(); WorldWindowRegistry.INSTANCE.addSelectListener(this); } /** * Start the earthquake RSS feed download, performing it on a separate * daemon thread. */ protected void startEarthquakeDownload() { Thread thread = new Thread(new Runnable() { @Override public void run() { downloadEarthquakes(); } }); thread.setDaemon(true); thread.start(); } /** * Download the earthquake RSS feed. */ protected void downloadEarthquakes() { setLoading(true); try { RetrievalHandler handler = new RetrievalHandler() { @Override public void handle(RetrievalResult result) { synchronized (this) { try { DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setNamespaceAware(true); // Required to account for the georss namespace used DocumentBuilder builder = builderFactory.newDocumentBuilder(); StringReader reader = new StringReader(result.getAsString().trim()); InputSource source = new InputSource(reader); Document document = builder.parse(source); Element[] items = WWXML.getElements(document.getDocumentElement(), "//item", null); if (items != null) { List<Earthquake> earthquakes = new ArrayList<RSSEarthquakesLayer.Earthquake>(items.length); for (Element item : items) { earthquakes.add(new Earthquake(item)); } clearRenderables(); for (Earthquake earthquake : earthquakes) { addEarthquake(earthquake); } addRenderable(tooltipAnnotation); } } catch (Exception e) { e.printStackTrace(); } setLoading(false); firePropertyChange(AVKey.LAYER, null, this); } } }; URL url = new URL(RSS_URL); Downloader.downloadAnyway(url, handler, handler, true); } catch (Exception e) { e.printStackTrace(); } } /** * Add an earthquake to this layer. Called by the {@link RetrievalHandler} * of the download. * * @param earthquake * Earthquake to add. */ protected void addEarthquake(Earthquake earthquake) { if (attributes == null) { // Init default attributes for all eq attributes = new AnnotationAttributes(); attributes.setLeader(AVKey.SHAPE_NONE); attributes.setDrawOffset(new Point(0, -16)); attributes.setSize(new Dimension(32, 32)); attributes.setBorderWidth(0); attributes.setCornerRadius(0); attributes.setBackgroundColor(new Color(0, 0, 0, 0)); } Position surfacePosition = new Position(earthquake.position, 0); SurfaceEarthquakeAnnotation surfaceAnnotation = new SurfaceEarthquakeAnnotation(surfacePosition, earthquake, attributes); long time = MAX_TIME; if (earthquake.date != null) { // Compute days since Date now = new Date(); time = now.getTime() - earthquake.date.getTime(); // Update latestEq if (this.latestEq == null || this.latestEq.earthquake.date.getTime() < earthquake.date.getTime()) { this.latestEq = surfaceAnnotation; } } double percent = Math.max(0, Math.min(1, time / (double) MAX_TIME)); HSLColor hslColor = new HSLColor((float) percent * 240f, 80f, 50f); Color color = hslColor.getRGB(); surfaceAnnotation.getAttributes().setTextColor(color); surfaceAnnotation.getAttributes().setScale(earthquake.magnitude.doubleValue() / 10); addRenderable(surfaceAnnotation); EarthquakeAnnotation deepAnnotation = new SubSurfaceEarthquakeAnnotation(earthquake.position, earthquake, attributes, surfaceAnnotation); deepAnnotation.getAttributes().setTextColor(color); deepAnnotation.getAttributes().setScale(earthquake.magnitude.doubleValue() / 10); deepAnnotation.setPickEnabled(false); addRenderable(deepAnnotation); } @Override public void selected(SelectEvent event) { Object o = event.getTopObject(); if (event.getEventAction().equals(SelectEvent.ROLLOVER)) { highlight(o); } else if (event.getEventAction().equals(SelectEvent.LEFT_CLICK)) { click(o); } if (event.getSource() instanceof Component) { Cursor cursor = (o instanceof SurfaceEarthquakeAnnotation) ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null; ((Component) event.getSource()).setCursor(cursor); } } private void highlight(Object o) { if (this.mouseEq == o) { return; // same thing selected } if (this.mouseEq != null) { this.mouseEq.getAttributes().setHighlighted(false); this.mouseEq = null; this.tooltipAnnotation.getAttributes().setVisible(false); } if (o != null && o instanceof SurfaceEarthquakeAnnotation) { this.mouseEq = (SurfaceEarthquakeAnnotation) o; this.mouseEq.getAttributes().setHighlighted(true); this.tooltipAnnotation.setText(createTooltipAnnotationFromEarthquake(mouseEq.earthquake)); this.tooltipAnnotation.setPosition(this.mouseEq.getPosition()); this.tooltipAnnotation.getAttributes().setVisible(true); } } private String createTooltipAnnotationFromEarthquake(Earthquake quake) { final SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss z"); StringBuilder result = new StringBuilder(); result.append("<p><b>").append(quake.title).append("</b></p>").append(dateFormat.format(quake.date)) .append("<br/>").append("Magnitude: <b>").append(quake.magnitude).append("</b><br/>") .append("Depth: <b>").append((quake.position.elevation / -1000)).append(" km</b>"); return result.toString(); } private void click(Object o) { if (o != null && o instanceof SurfaceEarthquakeAnnotation) { String link = ((SurfaceEarthquakeAnnotation) o).earthquake.link; try { DefaultLauncher.openURL(new URL(link)); } catch (MalformedURLException e) { e.printStackTrace(); } } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (enabled != updateTimer.isRunning()) { if (enabled) { updateTimer.start(); } else { updateTimer.stop(); } } } /** * Represents a single Earthquake occurrence. * <p/> * Contains constructors able to interpret the GA RSS feed for earthquake * data. */ public static class Earthquake { public final String title; public final Date date; public final String link; public final Position position; public final BigDecimal magnitude; private static final Pattern MAGNITUDE_TITLE_PATTERN = Pattern .compile("(?i)(?:magnitude )?(?-i)(\\d+(?:\\.\\d+)?),[\\s]*([\\w\\s'-,]*)"); private static final Pattern DATE_ELEVATION_PATTERN = Pattern.compile("(?s)(?i)date and time(?-i).*?(UTC:[\\s]?\\d+\\s+\\w+\\s+\\d{4}\\s+\\d{2}:\\d{2}:\\d{2}).*(?i)depth(?-i).*:\\s+(\\d+(?:\\.\\d+))"); private static final String DATE_FORMAT = "Z: dd MMMM yyyy HH:mm:ss"; public Earthquake(Element content) { if (content == null) { throw new IllegalArgumentException("An XML element is required."); } XPath xpath = WWXML.makeXPath(); // Add the georss namespace to the xpath MapBackedNamespaceContext context = new MapBackedNamespaceContext(); context.addMapping("georss", "http://www.georss.org/georss"); xpath.setNamespaceContext(context); // Use the link as-is link = WWXML.getText(content, "link", xpath); // Parse magnitude and title from the 'title' element String titleElement = WWXML.getText(content, "title", xpath); { Matcher matcher = MAGNITUDE_TITLE_PATTERN.matcher(titleElement); if (matcher.find()) { this.magnitude = new BigDecimal(matcher.group(1)); this.title = matcher.group(2); } else { this.magnitude = new BigDecimal(0); this.title = titleElement; } } // Parse location from the 'georss:point' element String pointElement = WWXML.getText(content, "georss:point", xpath); String[] latLonElements = pointElement.split(" "); LatLon latlon = LatLon.fromDegrees(Double.parseDouble(latLonElements[0]), Double.parseDouble(latLonElements[1])); // Parse date and depth from the 'description' element String descriptionElement = WWXML.getText(content, "description", xpath); double elevation = 0; Date theDate = null; { Matcher matcher = DATE_ELEVATION_PATTERN.matcher(descriptionElement); if (matcher.find()) { try { theDate = new SimpleDateFormat(DATE_FORMAT).parse(matcher.group(1)); } catch (ParseException e) { } elevation = Double.parseDouble(matcher.group(2)) * -1000; // km -> m } } this.date = theDate; this.position = new Position(latlon, elevation); } } /** * {@link GlobeAnnotation} subclass used for rendering a single earthquake. */ private abstract class EarthquakeAnnotation extends GlobeAnnotation { protected final Earthquake earthquake; protected DoubleBuffer shapeBuffer; public EarthquakeAnnotation(Position position, Earthquake earthquake, AnnotationAttributes defaults) { super("", position, defaults); this.earthquake = earthquake; } @Override protected void applyScreenTransform(DrawContext dc, int x, int y, int width, int height, double scale) { double finalScale = scale * this.computeScale(dc); GL2 gl = dc.getGL().getGL2(); gl.glTranslated(x, y, 0); gl.glScaled(finalScale, finalScale, 1); } @Override public void render(DrawContext dc) { //override to pass the annotation point to the AnnotationRenderer, so that //frustum culling works for subsurface annotations if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (!this.getAttributes().isVisible()) { return; } Vec4 annotationPoint = getAnnotationDrawPoint(dc); dc.getAnnotationRenderer().render(dc, this, annotationPoint, dc.getCurrentLayer()); } @Override protected void doDraw(DrawContext dc, int width, int height, double opacity, Position pickPosition) { // Draw colored circle around screen point - use annotation's text color if (dc.isPickingMode()) { this.bindPickableObject(dc, pickPosition); } this.applyColor(dc, this.getAttributes().getTextColor(), 0.6 * opacity, true); drawEarthquake(dc); } protected abstract void drawEarthquake(DrawContext dc); } /** * {@link EarthquakeAnnotation} subclass used for rendering earthquakes on * the surface of the globe. */ private class SurfaceEarthquakeAnnotation extends EarthquakeAnnotation { public SurfaceEarthquakeAnnotation(Position position, Earthquake earthquake, AnnotationAttributes defaults) { super(position, earthquake, defaults); } @Override protected void drawEarthquake(DrawContext dc) { // Draw 32x32 shape from its bottom left corner int size = 32; if (this.shapeBuffer == null) { this.shapeBuffer = FrameFactory.createShapeBuffer(AVKey.SHAPE_ELLIPSE, size, size, 0, null); } dc.getGL().getGL2().glTranslated(-size / 2, -size / 2, 0); FrameFactory.drawBuffer(dc, GL2.GL_TRIANGLE_FAN, this.shapeBuffer); } } /** * {@link EarthquakeAnnotation} subclass used for rendering earthquakes * below the globe's surface. Uses a line to visualize the earthquake's * depth. */ private class SubSurfaceEarthquakeAnnotation extends EarthquakeAnnotation { private final SurfaceEarthquakeAnnotation surfaceAnnotation; public SubSurfaceEarthquakeAnnotation(Position position, Earthquake earthquake, AnnotationAttributes defaults, SurfaceEarthquakeAnnotation surfaceAnnotation) { super(position, earthquake, defaults); this.surfaceAnnotation = surfaceAnnotation; } @Override protected void drawEarthquake(DrawContext dc) { // Draw 32x32 shape from its bottom left corner int size = 32; if (this.shapeBuffer == null) { this.shapeBuffer = FrameFactory.createShapeBuffer(AVKey.SHAPE_RECTANGLE, size, 4, 0, null); } dc.getGL().getGL2().glTranslated(-size / 2, -2, 0); FrameFactory.drawBuffer(dc, GL2.GL_TRIANGLE_FAN, this.shapeBuffer); //if this is the subsurface annotation, draw a connected line if (surfaceAnnotation != null) { Vec4 drawPoint = getAnnotationDrawPoint(dc); Vec4 surfacePoint = surfaceAnnotation.getAnnotationDrawPoint(dc); if (drawPoint != null && surfacePoint != null) { GL2 gl = dc.getGL().getGL2(); OGLStackHandler stack = new OGLStackHandler(); try { stack.pushModelview(gl); stack.pushProjection(gl); BasicView.loadGLViewState(dc, dc.getView().getModelviewMatrix(), dc.getView() .getProjectionMatrix()); gl.glLineWidth(1f); gl.glBegin(GL2.GL_LINES); { gl.glVertex3d(drawPoint.x, drawPoint.y, drawPoint.z); gl.glVertex3d(surfacePoint.x, surfacePoint.y, surfacePoint.z); } gl.glEnd(); } finally { stack.pop(gl); } } } } } protected void fireLoadingStateChanged() { for (int i = loadingListeners.size() - 1; i >= 0; i--) { LoadingListener listener = loadingListeners.get(i); listener.loadingStateChanged(this, isLoading()); } } protected void setLoading(boolean loading) { this.loading = loading; fireLoadingStateChanged(); } @Override public boolean isLoading() { return loading; } @Override public void addLoadingListener(LoadingListener listener) { loadingListeners.add(listener); } @Override public void removeLoadingListener(LoadingListener listener) { loadingListeners.remove(listener); } }