// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/EarthquakeLayer.java,v $
// $RCSfile: EarthquakeLayer.java,v $
// $Revision: 1.5.2.3 $
// $Date: 2007/04/24 19:55:46 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.swing.JButton;
import javax.swing.JPanel;
import com.bbn.openmap.event.MapMouseListener;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMPoint;
import com.bbn.openmap.omGraphics.OMText;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PaletteHelper;
import com.bbn.openmap.util.PropUtils;
/**
* Get data about recent earthquakes from the USGS finger sites and
* display it.
* <p>
* Debugging information is printed when the OpenMap Viewer is launch
* with -Ddebug.earthquake flag.
* <P>
*
* <pre>
*
*
* # Properties for the Earthquake Layer
* earthquake.sites=<finger site> <finger site> ...
* # in seconds
* earthquake.queryinterval=300
*
*
* </pre>
*/
public class EarthquakeLayer extends OMGraphicHandlerLayer implements
MapMouseListener {
public final static transient String fingerSitesProperty = "sites";
public final static transient String queryIntervalProperty = "queryInterval";
/**
* Sites to finger user the user `quake'.
*/
protected String fingerSites[] = { "scec.gps.caltech.edu",
"geophys.washington.edu", "giseis.alaska.edu", "mbmgsun.mtech.edu",
"quake.eas.slu.edu" };
// Old sites
// "gldfs.cr.usgs.gov",
// "andreas.wr.usgs.gov",
// "seismo.unr.edu",
// "eqinfo.seis.utah.edu",
// "sisyphus.idbsu.edu",
// "info.seismo.usbr.gov",
// "vtso.geol.vt.edu",
// "tako.wr.usgs.gov",
// "ldeo.columbia.edu"
/**
* Sites that are actively being queried.
*/
protected boolean activeSites[] = new boolean[fingerSites.length];
/** Default to 5 minutes. */
private long fetchIntervalMillis = 300 * 1000;
// lat-lon data of the earthquakes
protected float llData[] = new float[0];
// floating information about the earthquakes
protected String infoData[] = new String[0];
// floating information about the earthquakes
protected String drillData[] = new String[0];
private long lastDataFetchTime = 0;
protected Color lineColor = Color.red;
protected boolean showingInfoLine = false;
/** The layer GUI. */
protected JPanel gui = null;
/**
* Construct an EarthquakeLayer.
*/
public EarthquakeLayer() {
activeSites[0] = true;
setProjectionChangePolicy(new com.bbn.openmap.layer.policy.ListResetPCPolicy(this));
}
/**
* Fetch data from finger sites, if needed, generate the
* OMGraphics with the current projection regardless.
*/
public synchronized OMGraphicList prepare() {
if (needToRefetchData()) {
parseData(getEarthquakeData());
}
return generateGraphics();
}
/**
* Fetches data if it hasn't been fetched in a while.
*/
protected boolean needToRefetchData() {
long now = System.currentTimeMillis();
long last = lastDataFetchTime;
if ((last + fetchIntervalMillis) < now) {
lastDataFetchTime = now;
return true;
}
return false;
}
/**
* Create the graphics.
*/
protected OMGraphicList generateGraphics() {
OMGraphicList omgraphics = new OMGraphicList();
OMPoint circ;
OMText text;
int circle_r = 2;
int circle_h = 5;
for (int i = 0, j = 0; i < llData.length; i += 2, j++) {
// grouping
OMGraphicList group = new OMGraphicList(2);
// XY-Circle at LatLonPoint
circ = new OMPoint(llData[i], llData[i + 1], circle_r);
circ.setOval(true);
circ.setFillPaint(lineColor);
group.add(circ);
// Info
text = new OMText(llData[i], llData[i + 1], 0, circle_h + 10, infoData[j], java.awt.Font.decode("SansSerif"), OMText.JUSTIFY_CENTER);
text.setLinePaint(lineColor);
group.add(text);
group.setAppObject(new Integer(j));//remember index
omgraphics.add(group);
}
omgraphics.generate(getProjection(), false);
return omgraphics;
}
/**
* Parse the finger site data.
*
* @param data Vector
*/
protected void parseData(Vector data) {
int nLines = data.size();
llData = new float[2 * nLines];
infoData = new String[nLines];
drillData = new String[nLines];
for (int i = 0, j = 0, k = 0; i < nLines; i++) {
String line = (String) data.elementAt(i);
// Read a line of input and break it down
StringTokenizer tokens = new StringTokenizer(line);
String sdate = tokens.nextToken();
String stime = tokens.nextToken();
String slat = tokens.nextToken();
String slon = tokens.nextToken();
if (slon.startsWith("NWSE"))// handle ` ' in LatLon data
slon = tokens.nextToken();
String sdep = tokens.nextToken();
if (sdep.startsWith("NWSE"))// handle ` ' in LatLon data
sdep = tokens.nextToken();
String smag = tokens.nextToken();
String q = tokens.nextToken();
String scomment = tokens.nextToken("\r\n");
if (q.length() > 1) {
scomment = q + " " + scomment;
}
infoData[j] = smag;
drillData[j++] = sdate + " " + stime + " (UTC) " + slat + " "
+ slon + " " + smag + " " + scomment;
// Remove NESW from lat and lon before converting to float
int west = slon.indexOf("W");
int south = slat.indexOf("S");
if (west >= 0)
slon = slon.replace('W', '\0');
else
slon = slon.replace('E', '\0');
if (south >= 0)
slat = slat.replace('S', '\0');
else
slat = slat.replace('N', '\0');
slon = slon.trim();
slat = slat.trim();
float flat = 0, flon = 0;
try {
flat = new Float(slat).floatValue();
flon = new Float(slon).floatValue();
} catch (NumberFormatException e) {
Debug.error("EarthquakeLayer.parseData(): " + e + " line: "
+ line);
}
// replace West and South demarcations with minus sign
if (south >= 0)
flat = -flat;
if (west >= 0)
flon = -flon;
llData[k++] = flat;
llData[k++] = flon;
}
}
/**
* Get the earthquake data from the USGS. Should be called in a
* SwingWorker thread, or you will freeze the application.
*
* @return Vector containing information from the websites.
*/
protected Vector getEarthquakeData() {
Vector linesOfData = new Vector();
Socket quakefinger = null;
PrintWriter output = null;
BufferedReader input = null;
String line;
for (int i = 0; i < activeSites.length; i++) {
// skip sites which aren't on the active list
if (!activeSites[i])
continue;
try {
if (Debug.debugging("earthquake")) {
Debug.output("Opening socket connection to "
+ fingerSites[i]);
}
quakefinger = new Socket(fingerSites[i], 79);//open
// connection
// to
// finger
// port
quakefinger.setSoTimeout(120 * 1000);// 2 minute
// timeout
output = new PrintWriter(new OutputStreamWriter(quakefinger.getOutputStream()), true);
input = new BufferedReader(new InputStreamReader(quakefinger.getInputStream()), 1);
output.println("/W quake");// use `/W' flag for long
// output
} catch (IOException e) {
Debug.error("EarthquakeLayer.getEarthquakeData(): "
+ "can't open or write to socket: " + e);
continue;
}
try {
// add data lines to list
while ((line = input.readLine()) != null) {
if (Debug.debugging("earthquake")) {
Debug.output("EarthquakeLayer.getEarthQuakeData(): "
+ line);
}
if (line.length() == 0)
continue;
if (!Character.isDigit(line.charAt(0)))
continue;
line = hackY2K(line);
if (line == null)
continue;
linesOfData.addElement(line);
}
} catch (IOException e) {
Debug.error("EarthquakeLayer.getEarthquakeData(): "
+ "can't read from the socket: " + e);
if (isCancelled()) {
return null;
}
}
try {
quakefinger.close();
} catch (IOException e) {
Debug.error("EarthquakeLayer.getEarthquakeData(): "
+ "error closing socket: " + e);
}
}
// int nQuakes = linesOfData.size();
// for (int i=0; i<nQuakes; i++) {
// Debug.output((String)linesOfData.elementAt(i));
// }
return linesOfData;
}
// This is the USGS's date problem, not ours (of course when they
// change their format, we'll have to update this).
// Note that also this could just be a bogus line (not a dataline)
// beginning with a number, so we've got to deal with it here.
private String hackY2K(String date) {
StringTokenizer tok = new StringTokenizer(date, "/");
String year, month, day;
try {
year = tok.nextToken();
month = tok.nextToken();
day = tok.nextToken();
} catch (NoSuchElementException e) {
Debug.error("EarthquakeLayer: unparsable date: " + date);
return null;
}
if (year.length() == 2) {
int y;
try {
y = Integer.parseInt(year);
} catch (NumberFormatException e) {
Debug.error("EarthquakeLayer: invalid year: " + year);
return null;
}
// Sliding window technique...
if (y > 70) {
date = "19";
} else {
date = "20";
}
} else if (year.length() != 4) {
Debug.error("EarthquakeLayer: unparsable year: " + year);
return null;
}
date = date + year + "/" + month + "/" + day;
return date;
}
/**
* Gets the gui controls associated with the layer.
*
* @return Component
*/
public Component getGUI() {
JPanel p;
if (gui == null) {
gui = PaletteHelper.createVerticalPanel("Earthquakes");
GridBagLayout gridbag = new GridBagLayout();
GridBagConstraints constraints = new GridBagConstraints();
gui.setLayout(gridbag);
constraints.fill = GridBagConstraints.HORIZONTAL; // fill
// horizontally
constraints.gridwidth = GridBagConstraints.REMAINDER; //another
// row
constraints.anchor = GridBagConstraints.EAST; // tack to
// the left
// edge
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
int index = Integer.parseInt(e.getActionCommand(), 10);
activeSites[index] = !activeSites[index];
}
};
p = PaletteHelper.createCheckbox("Sites",
fingerSites,
activeSites,
al);
gridbag.setConstraints(p, constraints);
gui.add(p);
JButton b = new JButton("Query Now");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// force refetch of data
lastDataFetchTime = 0;
doPrepare();
}
});
gridbag.setConstraints(p, constraints);
gui.add(b);
}
return gui;
}
/**
* Returns the MapMouseListener object that handles the mouse
* events.
*
* @return the MapMouseListener for the layer, or null if none
*/
public MapMouseListener getMapMouseListener() {
return this;
}
//----------------------------------------------------------------
// MapMouseListener interface methods
//----------------------------------------------------------------
/**
* Return a list of the modes that are interesting to the
* MapMouseListener. The source MouseEvents will only get sent to
* the MapMouseListener if the mode is set to one that the
* listener is interested in. Layers interested in receiving
* events should register for receiving events in "select" mode:
* <code>
* <pre>
* return new String[] { SelectMouseMode.modeID };
* </pre>
* <code>
* @return String[] of modeID's
* @see com.bbn.openmap.event.NavMouseMode#modeID
* @see com.bbn.openmap.event.SelectMouseMode#modeID
* @see com.bbn.openmap.event.NullMouseMode#modeID
*/
public String[] getMouseModeServiceList() {
return new String[] { com.bbn.openmap.event.SelectMouseMode.modeID };
}
/**
* Invoked when a mouse button has been pressed on a component.
*
* @param e MouseEvent
* @return true if the listener was able to process the event.
*/
public boolean mousePressed(MouseEvent e) {
return false;
}
/**
* Invoked when a mouse button has been released on a component.
*
* @param e MouseEvent
* @return true if the listener was able to process the event.
*/
public boolean mouseReleased(MouseEvent e) {
OMGraphicList omgraphics = getList();
if (omgraphics != null && drillData != null) {
OMGraphic obj = omgraphics.findClosest(e.getX(), e.getY(), 4);
if (obj != null) {
int id = ((Integer) obj.getAppObject()).intValue();
fireRequestInfoLine(drillData[id]);
showingInfoLine = true;
return true;
}
}
return false;
}
/**
* Invoked when the mouse has been clicked on a component. The
* listener will receive this event if it successfully processed
* <code>mousePressed()</code>, or if no other listener
* processes the event. If the listener successfully processes
* mouseClicked(), then it will receive the next mouseClicked()
* notifications that have a click count greater than one.
*
* @param e MouseEvent
* @return true if the listener was able to process the event.
*/
public boolean mouseClicked(MouseEvent e) {
return false;
}
/**
* Invoked when the mouse enters a component.
*
* @param e MouseEvent
*/
public void mouseEntered(MouseEvent e) {}
/**
* Invoked when the mouse exits a component.
*
* @param e MouseEvent
*/
public void mouseExited(MouseEvent e) {}
/**
* Invoked when a mouse button is pressed on a component and then
* dragged. The listener will receive these events if it
* successfully processes mousePressed(), or if no other listener
* processes the event.
*
* @param e MouseEvent
* @return true if the listener was able to process the event.
*/
public boolean mouseDragged(MouseEvent e) {
return false;
}
/**
* Invoked when the mouse button has been moved on a component
* (with no buttons down).
*
* @param e MouseEvent
* @return true if the listener was able to process the event.
*/
public boolean mouseMoved(MouseEvent e) {
// clean up display
if (showingInfoLine) {
showingInfoLine = false;
fireRequestInfoLine("");
}
return false;
}
/**
* Handle a mouse cursor moving without the button being pressed.
* This event is intended to tell the listener that there was a
* mouse movement, but that the event was consumed by another
* layer. This will allow a mouse listener to clean up actions
* that might have happened because of another motion event
* response.
*/
public void mouseMoved() {}
//----------------------------------------------------------------
// PropertyConsumer Interface
//----------------------------------------------------------------
/**
* Set the properties of the EarthquakeLayer.
*
* @param prefix String
* @param props Properties
*/
public void setProperties(String prefix, Properties props) {
super.setProperties(prefix, props);
prefix = PropUtils.getScopedPropertyPrefix(prefix);
// list of sites
String sites = props.getProperty(prefix + fingerSitesProperty);
if (sites != null) {
Vector v = new Vector();
String str;
StringTokenizer tok = new StringTokenizer(sites);
while (tok.hasMoreTokens()) {
str = tok.nextToken();
v.addElement(str);
}
int len = v.size();
fingerSites = new String[len];
activeSites = new boolean[len];
activeSites[0] = true;
for (int i = 0; i < len; i++) {
fingerSites[i] = (String) v.elementAt(i);
}
}
fetchIntervalMillis = PropUtils.intFromProperties(props, prefix
+ queryIntervalProperty, 300) * 1000;
}
/**
* Get the associated properties object.
*/
public Properties getProperties(Properties props) {
props = super.getProperties(props);
return getProperties(propertyPrefix, props);
}
/**
* Get the associated properties object. This method creates a
* Properties object if necessary and fills it with the relevant
* data for this layer. Relevant properties for EarthquakeLayers
* are the sites to retrieve earth quake data from, and the
* interval in milliseconds (see class description.)
*/
public Properties getProperties(String prefix, Properties props) {
props = super.getProperties(props);
prefix = PropUtils.getScopedPropertyPrefix(prefix);
StringBuffer sitesToFinger = new StringBuffer("");
for (int i = 0; i < fingerSites.length; ++i) {
sitesToFinger.append(fingerSites[i]);
sitesToFinger.append(" ");
}
sitesToFinger.deleteCharAt(sitesToFinger.length() - 1);
props.put(prefix + fingerSitesProperty, sitesToFinger.toString());
props.put(prefix + queryIntervalProperty,
Long.toString(fetchIntervalMillis));
return props;
}
/**
* Supplies the propertiesInfo object associated with this
* EarthquakeLayer object. Contains the human readable
* describtions of the properties and the
* <code>initPropertiesProperty</code> (see Inspector class.)
*/
public Properties getPropertyInfo(Properties info) {
info = super.getPropertyInfo(info);
info.put(fingerSitesProperty, "WWW sites to finger");
info.put(queryIntervalProperty, "Query interval in seconds");
return info;
}
}