// **********************************************************************
//
// <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/location/csv/CSVLocationHandler.java,v $
// $RCSfile: CSVLocationHandler.java,v $
// $Revision: 1.9.2.3 $
// $Date: 2005/08/09 18:18:46 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.location.csv;
/* Java */
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import com.bbn.openmap.layer.location.AbstractLocationHandler;
import com.bbn.openmap.layer.location.Location;
import com.bbn.openmap.layer.location.LocationCBMenuItem;
import com.bbn.openmap.layer.location.LocationHandler;
import com.bbn.openmap.layer.location.LocationLayer;
import com.bbn.openmap.layer.location.LocationMenuItem;
import com.bbn.openmap.layer.location.LocationPopupMenu;
import com.bbn.openmap.layer.location.URLRasterLocation;
import com.bbn.openmap.util.CSVTokenizer;
import com.bbn.openmap.util.DataOrganizer;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PropUtils;
import com.bbn.openmap.util.quadtree.QuadTree;
/**
* The CSVLocationLayer is a LocationHandler designed to let you put
* data on the map based on information from a Comma Separated
* Value(CSV) file. It's assumed that the each row in the file refers
* to a certain location, and that location contains a name label, a
* latitude and a longitude (both in decimal degrees).
*
* <P>
* The individual fields must not have leading whitespace.
*
* <P>
* The CSVLocationLayer gives you some basic functionality. The
* properties file lets you set defaults on whether to draw the
* locations and the names by default. For crowded layers, having all
* the names displayed might cause a cluttering problem. In gesture
* mode, OpenMap will display the name of each location as the mouse
* is passed over it. Pressing the left mouse button over a location
* brings up a popup menu that lets you show/hide the name label, and
* also to display the entire row contents of the location CSV file in
* a Browser window that OpenMap launches.
*
* <P>
* If you want to extend the functionality of this LocationHandler,
* there are a couple of methods to focus your changes: The
* setProperties() method lets you add properties to set from the
* properties file. The createData() method, by default, is a one-time
* method that creates the graphic objects based on the CSV data. By
* modifying these methods, and creating a different combination
* graphic other than the CSVLocation, you can create different layer
* effects pretty easily.
*
* <P>
* The locationFile property should contain a URL referring to the
* file. This can take the form of file:/myfile.csv for a local file
* or http://somehost.org/myfile.csv for a remote file.
*
* <P>
* In the openmap.properties file (for instance): <BR>
*
* <pre>
*
*
*
* # In the section for the LocationLayer:
* locationLayer.locationHandlers=csvlocationhandler
*
* csvlocationhandler.class=com.bbn.openmap.layer.location.csv.CSVLocationHandler
* csvlocationhandler.locationFile=/data/worldpts/WorldLocs_point.csv
* csvlocationhandler.csvFileHasHeader=true
* csvlocationhandler.locationColor=FF0000
* csvlocationhandler.nameColor=008C54
* csvlocationhandler.showNames=false
* csvlocationhandler.showLocations=true
* csvlocationhandler.nameIndex=0
* csvlocationhandler.latIndex=8
* csvlocationhandler.lonIndex=10
* # Optional property, if you have a column in the file for URLs of
* # images to use for an icon.
* csvlocationhandler.iconIndex=11
* # Optional property, URL of image to use as marker for all entries in
* # csv file without a URL listed at the iconIndex.
* csvlocationhandler.defaultIconURL=/data/symbols/default.gif
* # Optional property, if the eastern hemisphere longitudes are negative. False by default.
* csvlocationhandler.eastIsNeg=false
*
* # CSVLocationHandler has been updated to have regular DrawingAttribute properties for both name and location.
* csvlocationhandler.name.lineColor=FF008C54
* csvlocationhandler.location.lineColor=FFFF0000
* csvlocationhandler.location.fillColor=FFaaaaaa
* csvlocationhandler.location.pointRadius=3
* csvlocationhandler.location.pointOval=true
* # The old nameColor and locationColor properties will still work, and will take precidence over these DrawingAttribtues properties.
*
* </pre>
*/
public class CSVLocationHandler extends AbstractLocationHandler implements
LocationHandler, ActionListener {
/** The path to the primary CSV file holding the locations. */
protected String locationFile;
/** The property describing the locations of location data. */
public static final String LocationFileProperty = "locationFile";
/** Set if the CSVFile has a header record. Default is false. */
public final static String csvHeaderProperty = "csvFileHasHeader";
/** The storage mechanism for the locations. */
protected QuadTree quadtree = null;
/** The property describing whether East is a negative value. */
public static final String eastIsNegProperty = "eastIsNeg";
/** Are east values really negative with this file? */
protected boolean eastIsNeg = false;
/**
* Flag that specifies that the first line consists of header
* information, and should not be mapped to a graphic.
*/
protected boolean csvHasHeader = false;
// /////////////////////
// Name label variables
/** Index of column in CSV to use as name of location. */
protected int nameIndex = -1;
/**
* Property to use to designate the column of the CSV file to use
* as a name.
*/
public static final String NameIndexProperty = "nameIndex";
// //////////////////////
// Location Variables
/**
* Property to use to designate the column of the CSV file to use
* as the latitude.
*/
public static final String LatIndexProperty = "latIndex";
/**
* Property to use to designate the column of the CSV file to use
* as the longitude.
*/
public static final String LonIndexProperty = "lonIndex";
/**
* Property to use to designate the column of the CSV file to use
* as an icon URL
*/
public static final String IconIndexProperty = "iconIndex";
/**
* Property to set an URL for an icon image to use for all the
* locations that don't have an image defined in the csv file, or
* if there isn't an icon defined in the csv file for any of the
* locations and you want them all to have the same icon.
*/
public static final String DefaultIconURLProperty = "defaultIconURL";
/** Index of column in CSV to use as latitude of location. */
protected int latIndex = -1;
/** Index of column in CSV to use as logitude of location. */
protected int lonIndex = -1;
/** Index of column in CSV to use as URL of the icon. */
protected int iconIndex = -1;
protected String defaultIconURL = null;
/**
* The default constructor for the Layer. All of the attributes
* are set to their default values.
*/
public CSVLocationHandler() {}
/**
* The properties and prefix are managed and decoded here, for the
* standard uses of the CSVLocationHandler.
*
* @param prefix string prefix used in the properties file for
* this layer.
* @param properties the properties set in the properties file.
*/
public void setProperties(String prefix, java.util.Properties properties) {
super.setProperties(prefix, properties);
prefix = PropUtils.getScopedPropertyPrefix(prefix);
locationFile = properties.getProperty(prefix + LocationFileProperty);
latIndex = PropUtils.intFromProperties(properties, prefix
+ LatIndexProperty, -1);
lonIndex = PropUtils.intFromProperties(properties, prefix
+ LonIndexProperty, -1);
iconIndex = PropUtils.intFromProperties(properties, prefix
+ IconIndexProperty, -1);
nameIndex = PropUtils.intFromProperties(properties, prefix
+ NameIndexProperty, -1);
eastIsNeg = PropUtils.booleanFromProperties(properties, prefix
+ eastIsNegProperty, false);
defaultIconURL = properties.getProperty(prefix + DefaultIconURLProperty);
if (defaultIconURL != null && defaultIconURL.trim() == "") {
// If it's empty, it should be null, otherwise it causes
// confusion later when the empty string can't be
// interpreted as a valid URL to an image file.
defaultIconURL = null;
}
csvHasHeader = PropUtils.booleanFromProperties(properties, prefix
+ csvHeaderProperty, false);
if (Debug.debugging("location")) {
Debug.output("CSVLocationHandler indexes:\n latIndex = "
+ latIndex + "\n lonIndex = " + lonIndex
+ "\n nameIndex = " + nameIndex + "\n has header = "
+ csvHasHeader);
}
}
/**
* PropertyConsumer method, to fill in a Properties object,
* reflecting the current values of the layer. If the layer has a
* propertyPrefix set, the property keys should have that prefix
* plus a separating '.' prepended to each propery key it uses for
* configuration.
*
* @param props a Properties object to load the PropertyConsumer
* properties into.
* @return Properties object containing PropertyConsumer property
* values. If getList was not null, this should equal
* getList. Otherwise, it should be the Properties object
* created by the PropertyConsumer.
*/
public Properties getProperties(Properties props) {
props = super.getProperties(props);
String prefix = PropUtils.getScopedPropertyPrefix(this);
props.put(prefix + "class", this.getClass().getName());
props.put(prefix + LocationFileProperty, PropUtils.unnull(locationFile));
props.put(prefix + eastIsNegProperty, new Boolean(eastIsNeg).toString());
props.put(prefix + csvHeaderProperty,
new Boolean(csvHasHeader).toString());
props.put(prefix + NameIndexProperty,
(nameIndex != -1 ? Integer.toString(nameIndex) : ""));
props.put(prefix + LatIndexProperty,
(latIndex != -1 ? Integer.toString(latIndex) : ""));
props.put(prefix + LonIndexProperty,
(lonIndex != -1 ? Integer.toString(lonIndex) : ""));
props.put(prefix + IconIndexProperty,
(iconIndex != -1 ? Integer.toString(iconIndex) : ""));
props.put(prefix + DefaultIconURLProperty,
PropUtils.unnull(defaultIconURL));
return props;
}
/**
* Method to fill in a Properties object with values reflecting
* the properties able to be set on this PropertyConsumer. The key
* for each property should be the raw property name (without a
* prefix) with a value that is a String that describes what the
* property key represents, along with any other information about
* the property that would be helpful (range, default value,
* etc.). This method takes care of the basic LocationHandler
* parameters, so any LocationHandlers that extend the
* AbstractLocationHandler should call this method, too, before
* adding any specific properties.
*
* @param list a Properties object to load the PropertyConsumer
* properties into. If getList equals null, then a new
* Properties object should be created.
* @return Properties object containing PropertyConsumer property
* values. If getList was not null, this should equal
* getList. Otherwise, it should be the Properties object
* created by the PropertyConsumer.
*/
public Properties getPropertyInfo(Properties list) {
list = super.getPropertyInfo(list);
list.put("class" + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.NonEditablePropertyEditor");
list.put(LocationFileProperty,
"URL of file containing location information.");
list.put(LocationFileProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.FUPropertyEditor");
list.put(eastIsNegProperty,
"Flag to note that negative latitude are over the eastern hemisphere.");
list.put(eastIsNegProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
list.put(NameIndexProperty,
"The column index, in the location file, of the location label text.");
list.put(LatIndexProperty,
"The column index, in the location file, of the latitudes.");
list.put(LonIndexProperty,
"The column index, in the location file, of the longitudes.");
list.put(IconIndexProperty,
"The column index, in the location file, of the icon for locations (optional).");
list.put(DefaultIconURLProperty,
"The URL of an image file to use as a default for the location markers (optional).");
list.put(csvHeaderProperty,
"Flag to note that the first line in the csv file is a header line and should be ignored.");
return list;
}
public void reloadData() {
quadtree = createData();
}
protected boolean checkIndexSettings() {
if (latIndex == -1 || lonIndex == -1) {
Debug.error("CSVLocationHandler: createData(): Index properties for Lat/Lon/Name are not set properly! lat index:"
+ latIndex + ", lon index:" + lonIndex);
return false;
}
Debug.message("csvlocation", "CSVLocationHandler: Reading File:"
+ locationFile + " NameIndex: " + nameIndex + " latIndex: "
+ latIndex + " lonIndex: " + lonIndex + " iconIndex: "
+ iconIndex + " eastIsNeg: " + eastIsNeg);
return true;
}
/**
* Look at the CSV file and create the QuadTree holding all the
* Locations.
*/
protected QuadTree createData() {
QuadTree qt = new QuadTree(90.0f, -180.0f, -90.0f, 180.0f, 100, 50f);
if (!checkIndexSettings()) {
return null;
}
BufferedReader streamReader = null;
int lineCount = 0;
Object token = null;
TokenDecoder tokenHandler = getTokenDecoder();
// readHeader should be set to true if the first line has
// been read, or if the csvHasHeader is false.
boolean readHeader = !csvHasHeader;
try {
// This lets the property be specified as a file name
// even if it's not specified as file:/<name> in
// the properties file.
URL csvURL = PropUtils.getResourceOrFileOrURL(null, locationFile);
if (csvURL == null) {
}
streamReader = new BufferedReader(new InputStreamReader(csvURL.openStream()));
CSVTokenizer csvt = new CSVTokenizer(streamReader);
token = csvt.token();
while (!csvt.isEOF(token)) {
int i = 0;
Debug.message("csvlocation",
"CSVLocationHandler| Starting a line | have"
+ (readHeader ? " " : "n't ") + "read header");
while (!csvt.isNewline(token) && !csvt.isEOF(token)) {
if (readHeader) {
tokenHandler.handleToken(token, i);
}
token = csvt.token();
// For some reason, the check above doesn't always
// work
if (csvt.isEOF(token)) {
break;
}
i++;
}
if (!readHeader) {
readHeader = true;
} else {
lineCount++;
tokenHandler.createAndAddObjectFromTokens(qt);
}
token = csvt.token();
}
} catch (java.io.IOException ioe) {
throw new com.bbn.openmap.util.HandleError(ioe);
} catch (ArrayIndexOutOfBoundsException aioobe) {
throw new com.bbn.openmap.util.HandleError(aioobe);
} catch (NumberFormatException nfe) {
throw new com.bbn.openmap.util.HandleError(nfe);
} catch (ClassCastException cce) {
Debug.error("Problem reading entries in " + locationFile
+ ", check your index settings, first column = 0.");
throw new com.bbn.openmap.util.HandleError(cce);
} catch (NullPointerException npe) {
Debug.error("Problem reading location file, check " + locationFile);
throw new com.bbn.openmap.util.HandleError(npe);
} catch (java.security.AccessControlException ace) {
throw new com.bbn.openmap.util.HandleError(ace);
}
Debug.message("csvlocation", "CSVLocationHandler | Finished File:"
+ locationFile + ", read " + lineCount + " locations");
try {
if (streamReader != null) {
streamReader.close();
}
} catch (java.io.IOException ioe) {
throw new com.bbn.openmap.util.HandleError(ioe);
}
if (lineCount == 0 && readHeader) {
Debug.output("CSVLocationHandler has read file, but didn't find any data.\n Check file for a header line, and make sure that the\n properties (csvFileHasHeader) is set properly for this CSVLocationHandler. Trying again without header...");
csvHasHeader = !csvHasHeader;
return createData();
}
return qt;
}
protected TokenDecoder getTokenDecoder() {
return new DefaultLocationDecoder();
}
/**
* When a new Location object needs to be created from data read
* in the CSV file, this method is called. This method lets you
* extend the CSVLocationLayer and easily set what kind of
* Location objects to use.
*
* @param lat latitude of location, decimal degrees.
* @param lon longitude of location, decimal degrees.
* @param name the label of the location.
* @param iconURL the String for a URL for an icon. Can be null.
* @return Location object for lat/lon/name/iconURL.
*/
protected Location createLocation(float lat, float lon, String name,
String iconURL) {
// This will turn into a regular location if iconURL is null.
Location loc = new URLRasterLocation(lat, lon, name, iconURL);
// let the layer handler default set these initially...
loc.setShowName(isShowNames());
loc.setShowLocation(isShowLocations());
loc.setLocationHandler(this);
getLocationDrawingAttributes().setTo(loc.getLocationMarker());
getNameDrawingAttributes().setTo(loc.getLabel());
loc.setDetails(name + " is at lat: " + lat + ", lon: " + lon);
if (iconURL != null) {
loc.setDetails(loc.getDetails() + " icon: " + iconURL);
}
Debug.message("csvlocation", "CSVLocationHandler " + loc.getDetails());
return loc;
}
/**
* @param ranFile the file to be read. The file pointer shoutd be
* set to the line you want read.
* @return Array of strings representing the values between the
* commas.
*/
protected String[] readCSVLineFromFile(BufferedReader ranFile,
String[] retPaths) {
if (ranFile != null) {
try {
String newLine = ranFile.readLine();
if (newLine == null)
return null;
StringTokenizer token = new StringTokenizer(newLine, ",");
int numPaths = token.countTokens();
if (retPaths == null) {
retPaths = new String[numPaths];
} else
numPaths = retPaths.length;
for (int i = 0; i < numPaths; i++) {
retPaths[i] = token.nextToken();
}
} catch (java.io.IOException ioe) {
return null;
} catch (java.util.NoSuchElementException nsee) {
Debug.output("CSVLocationHandler: readCSVLineFromFile: oops");
}
}
return retPaths;
}
/**
* Prepares the graphics for the layer. This is where the
* getRectangle() method call is made on the location.
* <p>
* Occasionally it is necessary to abort a prepare call. When this
* happens, the map will set the cancel bit in the LayerThread,
* (the thread that is running the prepare). If this Layer needs
* to do any cleanups during the abort, it should do so, but
* return out of the prepare asap.
*
*/
public Vector get(float nwLat, float nwLon, float seLat, float seLon,
Vector graphicList) {
// IF the quadtree has not been set up yet, do it!
if (quadtree == null) {
Debug.output("CSVLocationHandler: Figuring out the locations and names! (This is a one-time operation!)");
quadtree = createData();
}
if (quadtree != null) {
if (Debug.debugging("csvlocation")) {
Debug.output("CSVLocationHandler|CSVLocationHandler.get() ul.lon = "
+ nwLon
+ " lr.lon = "
+ seLon
+ " delta = "
+ (seLon - nwLon));
}
quadtree.get(nwLat, nwLon, seLat, seLon, graphicList);
}
return graphicList;
}
public void fillLocationPopUpMenu(LocationPopupMenu locMenu) {
LocationCBMenuItem lcbi = new LocationCBMenuItem(LocationHandler.showname, locMenu, getLayer());
lcbi.setState(locMenu.getLoc().isShowName());
locMenu.add(lcbi);
locMenu.add(new LocationMenuItem(showdetails, locMenu, getLayer()));
}
protected Box box = null;
/**
* Provides the palette widgets to control the options of showing
* maps, or attribute text.
*
* @return Component object representing the palette widgets.
*/
public Component getGUI() {
if (box == null) {
JCheckBox showCSVLocationCheck, showNameCheck, forceGlobalCheck;
JButton rereadFilesButton;
showCSVLocationCheck = new JCheckBox("Show Locations", isShowLocations());
showCSVLocationCheck.setActionCommand(showLocationsCommand);
showCSVLocationCheck.addActionListener(this);
showCSVLocationCheck.setToolTipText("<HTML><BODY>Show location markers on the map.</BODY></HTML>");
showNameCheck = new JCheckBox("Show Location Names", isShowNames());
showNameCheck.setActionCommand(showNamesCommand);
showNameCheck.addActionListener(this);
showNameCheck.setToolTipText("<HTML><BODY>Show location names on the map.</BODY></HTML>");
forceGlobalCheck = new JCheckBox("Override Location Settings", isForceGlobal());
forceGlobalCheck.setActionCommand(forceGlobalCommand);
forceGlobalCheck.addActionListener(this);
forceGlobalCheck.setToolTipText("<HTML><BODY>Make these settings override those set<BR>on the individual map objects.</BODY></HTML>");
rereadFilesButton = new JButton("Reload Data From Source");
rereadFilesButton.setActionCommand(readDataCommand);
rereadFilesButton.addActionListener(this);
rereadFilesButton.setToolTipText("<HTML><BODY>Reload the data file, and put these settings<br>on the individual map objects.</BODY></HTML>");
box = Box.createVerticalBox();
box.add(showCSVLocationCheck);
box.add(showNameCheck);
box.add(forceGlobalCheck);
box.add(rereadFilesButton);
}
return box;
}
// ----------------------------------------------------------------------
// ActionListener interface implementation
// ----------------------------------------------------------------------
/**
* The Action Listener method, that reacts to the palette widgets
* actions.
*/
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd == showLocationsCommand) {
JCheckBox locationCheck = (JCheckBox) e.getSource();
setShowLocations(locationCheck.isSelected());
if (Debug.debugging("location")) {
Debug.output("CSVLocationHandler::actionPerformed showLocations is "
+ isShowLocations());
}
getLayer().repaint();
} else if (cmd == showNamesCommand) {
JCheckBox namesCheck = (JCheckBox) e.getSource();
setShowNames(namesCheck.isSelected());
if (Debug.debugging("location")) {
Debug.output("CSVLocationHandler::actionPerformed showNames is "
+ isShowNames());
}
LocationLayer ll = getLayer();
if (namesCheck.isSelected() && ll.getDeclutterMatrix() != null
&& ll.getUseDeclutterMatrix()) {
ll.doPrepare();
} else {
ll.repaint();
}
} else if (cmd == forceGlobalCommand) {
JCheckBox forceGlobalCheck = (JCheckBox) e.getSource();
setForceGlobal(forceGlobalCheck.isSelected());
getLayer().repaint();
} else if (cmd == readDataCommand) {
Debug.output("Re-reading Locations file");
quadtree = null;
getLayer().doPrepare();
} else {
Debug.error("Unknown action command \"" + cmd
+ "\" in LocationLayer.actionPerformed().");
}
}
public interface TokenDecoder {
void handleToken(Object token, int column);
void createAndAddObjectFromTokens(DataOrganizer organizer);
}
public class DefaultLocationDecoder implements TokenDecoder {
protected String name;
protected float lat;
protected float lon;
protected String iconURL;
public DefaultLocationDecoder() {}
public void reset() {
name = null;
lat = 0f;
lon = 0f;
iconURL = null;
}
public void handleToken(Object token, int i) {
if (i == nameIndex) {
if (token instanceof Double) {
name = ((Double) token).toString();
} else {
name = (String) token;
}
} else if (i == latIndex) {
lat = ((Double) token).floatValue();
} else if (i == lonIndex) {
lon = ((Double) token).floatValue();
if (eastIsNeg) {
lon *= -1;
}
} else if (i == iconIndex) {
iconURL = (String) token;
}
}
public void createAndAddObjectFromTokens(DataOrganizer organizer) {
// Debug.output(iconURL);
if (iconURL == null && defaultIconURL != null) {
iconURL = defaultIconURL;
}
Location loc = createLocation(lat, lon, name, iconURL);
organizer.put(lat, lon, loc);
reset();
}
}
}