// **********************************************************************
//
// <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.12 $
// $Date: 2005/08/09 18:17:08 $
// $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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Level;
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.URLRasterLocation;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.util.CSVTokenizer;
import com.bbn.openmap.util.DataOrganizer;
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.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
*
* # optional, can be used if you override createLocation and need access to varying rendering attributes.
* # ra1, ra2 and ra3 would be used as keys in renderAttributes map. All GraphicAttributes properties are available, not
* # just lineColor.
*
* csvlocationhandler.renderAttributesList=ra1 ra2 ra3
* csvlocationhandler.ra1.lineColor=0xFFFF0000
* csvlocationhandler.ra2.lineColor=0xFF00FF00
* csvlocationhandler.ra3.lineColor=0xFF00FFFF
*
* </pre>
*/
public class CSVLocationHandler extends AbstractLocationHandler implements LocationHandler {
/** 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<Location> 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 longitude 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().length() == 0) {
// 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 (logger.isLoggable(Level.FINE)) {
logger.fine("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 property 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) {
logger.warning("CSVLocationHandler: createData(): Index properties for Lat/Lon/Name are not set properly! lat index:"
+ latIndex + ", lon index:" + lonIndex);
return false;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("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<Location> createData() {
QuadTree<Location> qt = new QuadTree<Location>(90.0f, -180.0f, -90.0f, 180.0f, 100, 50f);
if (!checkIndexSettings()) {
return 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) {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(csvURL.openStream()));
CSVTokenizer csvt = new CSVTokenizer(streamReader);
token = csvt.token();
List recordList = Collections.synchronizedList(new ArrayList(10));
while (!csvt.isEOF(token)) {
int i = 0;
if (logger.isLoggable(Level.FINE)) {
logger.fine("CSVLocationHandler| Starting a line | have"
+ (readHeader ? " " : "n't ") + "read header");
}
// Prepare it for the new row/record
recordList.clear();
while (!csvt.isNewline(token) && !csvt.isEOF(token)) {
if (readHeader) {
// tokenHandler.handleToken(token, i);
recordList.add(token);
}
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++;
createLocation(recordList, qt);
// tokenHandler.createAndAddObjectFromTokens(qt);
}
token = csvt.token();
}
csvt.close();
} else {
if (logger.isLoggable(Level.FINE)) {
logger.fine("couldn't figure out file: " + locationFile);
}
}
} 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) {
logger.warning("Problem reading entries in " + locationFile
+ ", check your index settings, first column = 0.");
throw new com.bbn.openmap.util.HandleError(cce);
} catch (NullPointerException npe) {
logger.warning("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);
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("CSVLocationHandler | Finished File:" + locationFile + ", read "
+ lineCount + " locations");
}
if (lineCount == 0 && readHeader) {
logger.fine("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;
}
/**
* This is the method called by create data with a row's worth of
* information stuffed in the record List. The indexes set in the properties
* should describe what each entry is.
*
* @param recordList a record/row of data from the csv file.
* @param qt the Quadtree to add the Location object, created from the row
* contents.
*/
protected void createLocation(List recordList, QuadTree<Location> qt) {
String name = tokenToString(recordList, nameIndex, "");
double lat = tokenToDouble(recordList, latIndex, 0.0);
double lon = tokenToDouble(recordList, lonIndex, 0.0, eastIsNeg);
String iconURL = tokenToString(recordList, iconIndex, defaultIconURL);
qt.put(lat, lon, createLocation(lat, lon, name, iconURL, recordList));
}
/**
* 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
* based on file contents. The lat/lon/name/icon path have already been
* decoded from the record List.
*
* @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.
* @param recordList the original List of Objects in case other entries in
* the row should affect how the Location object is configured.
* @return Location object for lat/lon/name/iconURL.
*/
protected Location createLocation(double lat, double lon, String name, String iconURL,
List recordList) {
// 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);
}
logger.fine("CSVLocationHandler " + loc.getDetails());
return loc;
}
/**
* Scope object to String. If anything goes wrong the default is returned.
*
* @param recordList the List for the record.
* @param index the index of the object to fetch.
* @param def default value
* @return String value
*/
protected String tokenToString(List recordList, int index, String def) {
try {
Object obj = recordList.get(index);
if (obj != null) {
return obj.toString();
}
} catch (Exception e) {
// just return default
}
return def;
}
/**
* Scope object to double if it's a number, or return default. If anything
* goes wrong the default is returned.
*
* @param recordList the List for the record.
* @param index the index of the object to fetch.
* @param def default value
* @return double value
*/
protected double tokenToDouble(List recordList, int index, double def) {
try {
Object obj = recordList.get(index);
if (obj instanceof Double) {
return ((Double) obj).doubleValue();
}
} catch (Exception e) {
}
return def;
}
/**
* Scope object to double if it's a number, or return default. Swap the sign
* if needed, if east is supposed to be a negative number. If anything goes
* wrong the default is returned.
*
* @param recordList the List for the record.
* @param index the index of the object to fetch.
* @param def default value
* @param swapSign multiply value by -1, say, if you know that the file has
* negative values for eastern hemisphere longitudes (hey, I've seen
* it).
* @return double value
*/
protected double tokenToDouble(List recordList, int index, double def, boolean swapSign) {
Double ret = tokenToDouble(recordList, index, def);
return swapSign ? -1 * ret : ret;
}
/**
* Prepares the graphics for the layer. This is where the getRectangle()
* method calls to build the OMGraphicList to draw.
*/
public OMGraphicList get(double nwLat, double nwLon, double seLat, double seLon,
OMGraphicList graphicList) {
if (graphicList == null) {
graphicList = new OMGraphicList();
graphicList.setTraverseMode(OMGraphicList.FIRST_ADDED_ON_TOP);
}
// IF the quadtree has not been set up yet, do it!
if (quadtree == null) {
logger.fine("CSVLocationHandler: Figuring out the locations and names! (This is a one-time operation!)");
quadtree = createData();
}
if (quadtree != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("CSVLocationHandler|CSVLocationHandler.get() ul.lon = " + nwLon
+ " lr.lon = " + seLon + " delta = " + (seLon - nwLon));
}
List<Location> hits = new ArrayList<Location>();
quadtree.get(nwLat, nwLon, seLat, seLon, hits);
graphicList.addAll(hits);
}
return graphicList;
}
/*
* (non-Javadoc)
*
* @see
* com.bbn.openmap.layer.location.LocationHandler#getItemsForPopupMenu(com
* .bbn.openmap.layer.location.Location)
*/
public List<Component> getItemsForPopupMenu(Location loc) {
List<Component> menuItems = new ArrayList<Component>();
menuItems.add(new LocationCBMenuItem(LocationHandler.showname, loc));
menuItems.add(new LocationMenuItem(showdetails, loc));
return menuItems;
}
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 showLocationCheck, showNameCheck, forceGlobalCheck;
JButton rereadFilesButton;
showLocationCheck = new JCheckBox("Show Locations", isShowLocations());
showLocationCheck.setActionCommand(showLocationsCommand);
showLocationCheck.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JCheckBox locationCheck = (JCheckBox) ae.getSource();
setShowLocations(locationCheck.isSelected());
if (logger.isLoggable(Level.FINE)) {
logger.fine("CSVLocationHandler::actionPerformed showLocations is "
+ isShowLocations());
}
getLayer().repaint();
}
});
showLocationCheck.setToolTipText("<HTML><BODY>Show location markers on the map.</BODY></HTML>");
showNameCheck = new JCheckBox("Show Location Names", isShowNames());
showNameCheck.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JCheckBox namesCheck = (JCheckBox) ae.getSource();
setShowNames(namesCheck.isSelected());
if (logger.isLoggable(Level.FINE)) {
logger.fine("CSVLocationHandler::actionPerformed showNames is "
+ isShowNames());
}
LocationLayer ll = getLayer();
if (namesCheck.isSelected() && ll.getDeclutterMatrix() != null
&& ll.getUseDeclutterMatrix()) {
ll.doPrepare();
} else {
ll.repaint();
}
}
});
showNameCheck.setToolTipText("<HTML><BODY>Show location names on the map.</BODY></HTML>");
forceGlobalCheck = new JCheckBox("Override Location Settings", isForceGlobal());
forceGlobalCheck.setActionCommand(forceGlobalCommand);
forceGlobalCheck.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JCheckBox forceGlobalCheck = (JCheckBox) ae.getSource();
setForceGlobal(forceGlobalCheck.isSelected());
getLayer().repaint();
}
});
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.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Re-reading Locations file");
}
quadtree = null;
getLayer().doPrepare();
}
});
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(showLocationCheck);
box.add(showNameCheck);
box.add(forceGlobalCheck);
box.add(rereadFilesButton);
}
return box;
}
}