// **********************************************************************
//
// <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/shape/SpatialIndex.java,v $
// $RCSfile: SpatialIndex.java,v $
// $Revision: 1.19 $
// $Date: 2009/02/25 22:34:04 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.shape;
import java.awt.geom.Point2D;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import com.bbn.openmap.dataAccess.shape.DbfHandler;
import com.bbn.openmap.dataAccess.shape.EsriGraphicFactory;
import com.bbn.openmap.dataAccess.shape.ShapeUtils;
import com.bbn.openmap.io.BinaryBufferedFile;
import com.bbn.openmap.io.BinaryFile;
import com.bbn.openmap.io.FormatException;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.coords.GeoCoordTransformation;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.DataBounds;
import com.bbn.openmap.util.PropUtils;
/**
* A Spatial Index is a variation on a Shape Index, adding the bounding box of
* the shape to the index.
* <p>
* The file has a 100 byte header identical to a Shape Index followed by <i>n
* </i> records.
*
* <b>Usage</b>
* <pre>
*
* <i>Dumps spatial index information, excluding bounding boxes to stdout.
* Useful for comparing to a shape index.</i>
* java com.bbn.openmap.layer.shape.SpatialIndex -d file.ssx
*
* <i>Dumps spatial index information including bounding boxes to stdout.</i>
* java com.bbn.openmap.layer.shape.SpatialIndex -d -b file.ssx
*
* <i>Creates spatial index <code>file.ssx</code> from shape file.shp</i>
* java com.bbn.openmap.layer.shape.SpatialIndex -c file.shp
*
* </pre>
*
* <b>Notes</b><p>
* When reading the Shape file, the content length is the length of the record's
* contents, exclusive of the record header (8 bytes). So the size that we need
* to read in from the Shape file is actually denoted as ((contentLength * 2) +
* 8). This converts from 16bit units to 8 bit bytes and adds the 8 bytes for
* the record header.
*
* @author Tom Mitchell
* @version $Revision: 1.19 $ $Date: 2009/02/25 22:34:04 $
* @see ShapeIndex
*/
public class SpatialIndex extends ShapeUtils {
public static Logger logger = Logger.getLogger("com.bbn.openmap.layer.shape.SpatialIndex");
/** Size of a shape file header in bytes. */
public final static int SHAPE_FILE_HEADER_LENGTH = 100;
/** Size of a shape file record header in bytes. */
public final static int SHAPE_RECORD_HEADER_LENGTH = 8;
/** Size of the spatial index header in bytes. */
public final static int SPATIAL_INDEX_HEADER_LENGTH = 100;
/** Size of the spatial index record in bytes. */
public final static int SPATIAL_INDEX_RECORD_LENGTH = 40;
/** Default size for shape record buffer. */
public final static int DEFAULT_SHAPE_RECORD_SIZE = 50000;
/** The shape file. */
protected BinaryFile shp;
/**
* The handler for dbf file information.
*/
protected DbfHandler dbf;
/** The icon to use for point objects. */
protected ImageIcon pointIcon;
/** The bounds of all the shapes in the shape file. */
protected ESRIBoundingBox bounds = null;
/**
* The file name for the shape file, for opening/reopening.
*/
protected String shpFileName;
/**
* A cached list of the SpatialIndex file entries, for repeated reference.
*/
protected List<Entry> entries;
/**
* A factory object to use to create OMGraphics from the shp file.
*/
EsriGraphicFactory factory = new EsriGraphicFactory();
/**
* Opens a spatial index file for reading based on the location of the
* provided shp file.
*
* @param shpFilename the name of the spatial index file
* @exception IOException if something goes wrong opening the file
*/
public SpatialIndex(String shpFilename) throws IOException {
this.shpFileName = shpFilename;
if (logger.isLoggable(Level.FINE)) {
logger.fine("SpatialIndex(" + shpFilename + ");");
}
}
/**
* Opens a spatial index file and it's associated shape file.
*
* @param ssxFilename the name of the spatial index file
* @param shpFilename the name of the shape file
* @exception IOException if something goes wrong opening the files
* @deprecated ssx file is figured based on the shp file path
*/
public SpatialIndex(String ssxFilename, String shpFilename) throws IOException {
this(shpFilename);
}
/**
* Figures out the ssx file name from the shp file name.
*
* @param shpFileName
* @return ssx file name from shape file name
*/
public static String ssx(String shpFileName) {
String ret = null;
if (shpFileName != null) {
ret = shpFileName.substring(0, shpFileName.indexOf(".shp")) + ".ssx";
}
return ret;
}
/**
* Figures out the dbf file name from the shp file name.
*
* @param shpFileName
* @return dbf file name created from shp file name.
*/
public static String dbf(String shpFileName) {
String ret = null;
if (shpFileName != null) {
ret = shpFileName.substring(0, shpFileName.indexOf(".shp")) + ".dbf";
}
return ret;
}
/**
* Get the box boundary containing all the shapes.
*/
public ESRIBoundingBox getBounds() {
return getBounds(null);
}
/**
* Method that can be overridden to adjust how the BinaryFile is created for
* shp files.
*
* @param shapeFileName The path to shape file, absolute, relative or URL
* @return BinaryFile
* @throws IOException if the shapeFileName can't be found.
*/
protected synchronized BinaryFile getShpFile(String shapeFileName) throws IOException {
return new BinaryBufferedFile(shapeFileName);
}
/**
* Returns the bounds of the shape file. If bounds don't exist, they are
* read from the shape file header.
*
* @param coordTransform
* @return null if bounds can't been read, otherwise ESRIBoundingBox.
*/
public ESRIBoundingBox getBounds(GeoCoordTransformation coordTransform) {
if (bounds == null) {
try {
if (shpFileName != null) {
BinaryFile shpFile = getShpFile(shpFileName);
EsriGraphicFactory.Header header = new EsriGraphicFactory.Header(shpFile, coordTransform);
DataBounds dataBounds = header.getDataBounds();
if (dataBounds != null) {
Point2D min = dataBounds.getMin();
Point2D max = dataBounds.getMax();
bounds = new ESRIBoundingBox(min.getX(), min.getY());
bounds.addPoint(max.getX(), max.getY());
}
shpFile.close();
}
} catch (IOException ioe) {
bounds = null;
} catch (FormatException fe) {
bounds = null;
}
}
return bounds;
}
/**
* Reset the bounds so they will be recalculated the next time a file is
* read.
*/
public void resetBounds() {
bounds = null;
}
/**
* Creates a record instance from the shape file data. Calls the appropriate
* record constructor based on the shapeType, and passes the buffer and
* offset to that constructor.
*
* @param shapeType the shape file's shape type, enumerated in
* <code>ShapeUtils</code>
* @param b the buffer pointing to the raw record data
* @param off the offset of the data starting point in the buffer
* @exception IOException if something goes wrong reading the file
* @see ShapeUtils
*/
public ESRIRecord makeESRIRecord(int shapeType, byte[] b, int off) throws IOException {
switch (shapeType) {
case SHAPE_TYPE_NULL:
return null;
case SHAPE_TYPE_POINT:
// return new ESRIPointRecord(b, off);
return new ESRIPointRecord(b, off, pointIcon);
case SHAPE_TYPE_POLYGON:
case SHAPE_TYPE_ARC:
// case SHAPE_TYPE_POLYLINE:
return new ESRIPolygonRecord(b, off);
case SHAPE_TYPE_MULTIPOINT:
logger.fine("SpatialIndex.makeESRIRecord: Arc NYI");
return null;
// return new ESRIMultipointRecord(b, off);
default:
return null;
}
}
/**
* Locates records in the shape file that intersect with the given
* rectangle. The spatial index is searched for intersections and the
* appropriate records are read from the shape file. Not really used
* anymore, except for old code. Use EsriGraphicList or EsriGraphicFactory
* instead.
*
* @param xmin the smaller of the x coordinates
* @param ymin the smaller of the y coordinates
* @param xmax the larger of the x coordinates
* @param ymax the larger of the y coordinates
* @return an array of records that intersect the given rectangle
* @exception IOException if something goes wrong reading the files
*/
public ESRIRecord[] locateRecords(double xmin, double ymin, double xmax, double ymax)
throws IOException, FormatException {
boolean gatherBounds = false;
if (bounds == null) {
bounds = new ESRIBoundingBox();
gatherBounds = true;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("locateRecords:\n\txmin: " + xmin + "; ymin: " + ymin + "\n\txmax: " + xmax
+ "; ymax: " + ymax);
}
byte ixRecord[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
int recNum = 0;
Vector<ESRIRecord> v = new Vector<ESRIRecord>();
int sRecordSize = DEFAULT_SHAPE_RECORD_SIZE;
byte sRecord[] = new byte[sRecordSize];
if (shpFileName == null) {
return null;
}
BinaryBufferedFile ssx = new BinaryBufferedFile(ssx(shpFileName));
if (shp == null) {
shp = getShpFile(shpFileName);
}
// Need to figure out what the shape type is...
ssx.seek(32);
// int shapeType = readLEInt(ssx);
// /
ssx.byteOrder(false);
int shapeType = ssx.readInteger();
// /
ssx.seek(100); // skip the file header
while (true) {
int result = ssx.read(ixRecord, 0, SPATIAL_INDEX_RECORD_LENGTH);
// if (result == -1) {
if (result <= 0) {
break;// EOF
} else {
recNum++;
double xmin2 = readLEDouble(ixRecord, 8);
double ymin2 = readLEDouble(ixRecord, 16);
double xmax2 = readLEDouble(ixRecord, 24);
double ymax2 = readLEDouble(ixRecord, 32);
if (logger.isLoggable(Level.FINER)) {
logger.finer("Looking at rec num " + recNum);
logger.finer(" " + xmin2 + ", " + ymin2 + "\n " + xmax2 + ", " + ymax2);
}
if (gatherBounds) {
bounds.addPoint(xmin2, ymin2);
bounds.addPoint(xmax2, ymax2);
}
if (intersects(xmin, ymin, xmax, ymax, xmin2, ymin2, xmax2, ymax2)) {
int offset = readBEInt(ixRecord, 0);
int byteOffset = offset * 2;
int contentLength = readBEInt(ixRecord, 4);
int recordSize = (contentLength * 2) + 8;
// System.out.print(".");
// System.out.flush();
if (recordSize < 0) {
logger.warning("SpatialIndex: supposed to read record size of "
+ recordSize);
break;
}
if (recordSize > sRecordSize) {
sRecordSize = recordSize;
if (logger.isLoggable(Level.FINER)) {
logger.finer("Shapefile SpatialIndex record size: " + sRecordSize);
}
sRecord = new byte[sRecordSize];
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("going to shp byteOffset = " + byteOffset
+ " for record size = " + recordSize + ", offset = " + offset
+ ", shape type = " + shapeType);
}
try {
shp.seek(byteOffset);
int nBytes = shp.read(sRecord, 0, recordSize);
if (nBytes < recordSize) {
logger.warning("Shapefile SpatialIndex expected " + recordSize
+ " bytes, but got " + nBytes + " bytes instead.");
}
ESRIRecord record = makeESRIRecord(shapeType, sRecord, 0);
v.addElement(record);
} catch (IOException ioe) {
logger.warning("SpatialIndex.locateRecords: IOException. ");
ioe.printStackTrace();
break;
}
}
}
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Processed " + recNum + " records");
logger.fine("Selected " + v.size() + " records");
}
int nRecords = v.size();
ssx.close();
shp.close();
shp = null;
ESRIRecord result[] = new ESRIRecord[nRecords];
v.copyInto(result);
return result;
}
/**
* The factory is used to filter and create OMGraphics from a shape file.
* This accessor is provided in order to allow you to modify the data
* projection it uses, or the line type.
*
* @return EsriGraphicFactory being used to create EsriGraphics from shape
* file.
*/
public EsriGraphicFactory getFactory() {
if (factory == null) {
factory = new EsriGraphicFactory();
// You can set this in the ShapeLayer if you want, replacing
// DrawingAttributes with GraphicAttributes with a LINETYPE set.
// factory.setLineType(OMGraphic.LINETYPE_GREATCIRCLE);
}
return factory;
}
public void setFactory(EsriGraphicFactory factory) {
this.factory = factory;
}
/**
* Locates OMGraphics in the shape file that intersect with the given
* rectangle. The spatial index is searched for intersections and the
* appropriate OMGraphics are created from the shape file.
*
* @param xmin the smaller of the x coordinates
* @param ymin the smaller of the y coordinates
* @param xmax the larger of the x coordinates
* @param ymax the larger of the y coordinates
* @param list OMGraphicList to add OMGraphics to and return, if null one
* will be created.
* @param drawingAttributes DrawingAttributes to set on the OMGraphics.
* @param mapProj the Map Projection for the OMGraphics so they can be
* generated right after creation.
* @param dataProj for pre-projected data, a coordinate translator for the
* data's projection to use to translate the coordinates to decimal
* degree lat/lon. Can be null to leave the coordinates untouched.
* @return an OMGraphicList containing OMGraphics that intersect the given
* rectangle
* @exception IOException if something goes wrong reading the files
*/
public OMGraphicList getOMGraphics(double xmin, double ymin, double xmax, double ymax,
OMGraphicList list, DrawingAttributes drawingAttributes,
Projection mapProj, GeoCoordTransformation dataProj)
throws IOException, FormatException {
if (logger.isLoggable(Level.FINE)) {
logger.fine("locateRecords:\n\txmin: " + xmin + "; ymin: " + ymin + "\n\txmax: " + xmax
+ "; ymax: " + ymax);
}
if (list == null) {
list = new OMGraphicList();
}
BinaryFile shpFile = shp;
DbfHandler dbfFile = dbf;
if (shpFile == null) {
shp = getShpFile(shpFileName);
shpFile = shp;
}
if (shpFile == null) {
return list;
}
EsriGraphicFactory.ReadByteTracker byteTracker = new EsriGraphicFactory.ReadByteTracker();
EsriGraphicFactory factory = getFactory();
factory.setDataCoordTransformation(dataProj);
OMGraphicList labels = new OMGraphicList();
list.add(labels);
for (Iterator<?> it = entryIterator(dataProj); it.hasNext();) {
Entry entry = (Entry) it.next();
if (entry.intersects(xmin, ymin, xmax, ymax)) {
try {
OMGraphic omg = (OMGraphic) factory.makeEsriGraphicFromRecord(entry.getByteOffset(), shpFile, drawingAttributes, pointIcon, byteTracker);
if (omg != null) {
if (dbfFile != null) {
omg = dbfFile.evaluate(omg, labels, mapProj);
if (omg == null) {
// Failed dbf test, should be ignored.
continue;
}
}
if (mapProj != null) {
omg.generate(mapProj);
}
list.add(omg);
}
} catch (IOException ioe) {
logger.warning("IOException message: " + ioe.getMessage());
ioe.printStackTrace();
break;
}
}
}
if (shpFile != null) {
shpFile.close();
// Not sure why we want to set this null here. It's cleaner, but a
// new
// BinaryFile object is created for every projection change.
// shp = null;
}
if (dbfFile != null) {
dbfFile.close();
}
return list;
}
/**
* Retrieves all OMGraphics in the shape file.
*
* @param retList OMGraphicList to add OMGraphics to and return, if null one
* will be created.
* @param drawingAttributes DrawingAttributes to set on the OMGraphics.
* @param mapProj the Map Projection for the OMGraphics so they can be
* generated right after creation. This will also be used by the
* DbfHandler, to determine if some OMGraphics should not be returned
* based on attribute settings.
* @param dataProj for preprojected data, a coordinate translator for the
* data's projection to use to translate the coordinates to decimal
* degree lat/lon. Can be null to leave the coordinates untouched.
* @return an OMGraphicList containing OMGraphics that intersect the given
* rectangle
* @exception IOException if something goes wrong reading the files
*/
public OMGraphicList getAllOMGraphics(OMGraphicList retList,
DrawingAttributes drawingAttributes, Projection mapProj,
GeoCoordTransformation dataProj)
throws IOException, FormatException {
if (retList == null) {
retList = new OMGraphicList();
}
if (shp == null) {
shp = getShpFile(shpFileName);
}
if (shp == null) {
return retList;
}
EsriGraphicFactory factory = getFactory();
factory.setDataCoordTransformation(dataProj);
factory.getEsriGraphics(shp, drawingAttributes, pointIcon, mapProj, retList);
shp.close();
return retList;
}
/**
* Takes the contents of the list and evaluates them against the information
* contained in the DbfHandler set in this SpatialIndex class.
*
* @param retList the list of OMGraphics to evaluate.
* @param mapProj the current map projection to be used by the DbfHandler to
* determine if some OMGraphics should be visible.
* @return OMGraphicList containing OMGraphics modified/passing evaluations
* rules in the DbfHandler.
*/
public OMGraphicList evaluateDbf(OMGraphicList retList, Projection mapProj) {
if (dbf != null) {
OMGraphicList labels = new OMGraphicList();
retList.add(labels);
OMGraphicList testList = new OMGraphicList();
for (OMGraphic omg : retList) {
if (omg != null) {
omg = dbf.evaluate(omg, labels, mapProj);
if (mapProj != null) {
omg.generate(mapProj);
}
testList.add(omg);
}
}
retList = testList;
}
return retList;
}
/**
* Evaluates the OMGraphic against the DbfHandler rules.
*
* @param omg the OMGraphic to evaluate.
* @param labels for DbfHandler label rules. Assumes that you are managing
* display of the labels list.
* @param mapProj for DbfHandler scale rules.
* @return OMGraphic if it passes the rules.
*/
public OMGraphic evaluate(OMGraphic omg, OMGraphicList labels, Projection mapProj) {
if (dbf != null) {
omg = dbf.evaluate(omg, labels, mapProj);
}
return omg;
}
/**
* Skips the BinaryFile for the shp data to the offset and reads the record
* data there, creating an OMGraphic from that data.
*
* @param byteOffset , usually gotten from an Entry object.
* @param drawingAttributes
* @return OMGraphic from entry object.
* @throws IOException
* @throws FormatException
*/
public OMGraphic getOMGraphicAtOffset(int byteOffset, DrawingAttributes drawingAttributes)
throws IOException, FormatException {
return (OMGraphic) getFactory().makeEsriGraphicFromRecord(byteOffset, shp, drawingAttributes, pointIcon, new EsriGraphicFactory.ReadByteTracker());
}
/**
* Provides an iterator over the SpatialIndex entries.
*
* @return iterator over entries
* @throws IOException
* @throws FormatException
*/
public Iterator<Entry> entryIterator() throws IOException, FormatException {
return entryIterator(null);
}
/**
* Provides an iterator over the SpatialIndex entries.
*
* @param dataTransform GeoCoordTransform for pre-projected data.
* @return iterator over entries, data transformed.
* @throws IOException
* @throws FormatException
*/
public Iterator<Entry> entryIterator(GeoCoordTransformation dataTransform)
throws IOException, FormatException {
List<Entry> entriesHandle = entries;
if (entriesHandle == null) {
boolean gatherBounds = false;
if (bounds == null) {
bounds = new ESRIBoundingBox();
gatherBounds = true;
}
entriesHandle = readIndexFile(gatherBounds ? bounds : null, dataTransform);
entries = entriesHandle;
}
return new ArrayList<Entry>(entriesHandle).iterator();
}
/**
*
* @param bounds if not null, add min/max values to them.
* @return list of entries.
* @throws IOException
* @throws FormatException
*/
protected List<Entry> readIndexFile(ESRIBoundingBox bounds) throws IOException, FormatException {
return readIndexFile(bounds, null);
}
/**
*
* @param bounds if not null, add min/max values to them.
* @param dataTransform GeoCoordTransform for pre-projected data.
* @return list of entries
* @throws IOException
* @throws FormatException
*/
protected List<Entry> readIndexFile(ESRIBoundingBox bounds, GeoCoordTransformation dataTransform)
throws IOException, FormatException {
List<Entry> entries = new ArrayList<Entry>();
byte ixRecord[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
if (shpFileName == null) {
return entries;
}
String ssxFileName = ssx(shpFileName);
if (!BinaryBufferedFile.exists(ssxFileName)) {
// If we got this far without an ssx existing, then we should just
// create one in memory.
return SpatialIndex.MemoryIndex.create(shpFileName);
}
BinaryBufferedFile ssx = new BinaryBufferedFile(ssxFileName);
ssx.byteOrder(false);
ssx.seek(100); // skip the file header
LatLonPoint llp = null;
if (dataTransform != null) {
llp = new LatLonPoint.Double();
}
while (true) {
int result = ssx.read(ixRecord, 0, SPATIAL_INDEX_RECORD_LENGTH);
if (result <= 0) {
break;// EOF
} else {
double xmin = readLEDouble(ixRecord, 8);
double ymin = readLEDouble(ixRecord, 16);
double xmax = readLEDouble(ixRecord, 24);
double ymax = readLEDouble(ixRecord, 32);
int byteOffset = readBEInt(ixRecord, 0) * 2;
if (dataTransform != null) {
llp = dataTransform.inverse(xmin, ymin, llp);
xmin = llp.getX();
ymin = llp.getY();
llp = dataTransform.inverse(xmax, ymax, llp);
xmax = llp.getX();
ymax = llp.getY();
}
if (logger.isLoggable(Level.FINER)) {
logger.finer("entry:\t" + xmin + ", " + ymin + "\n\t" + xmax + ", " + ymax);
}
Entry entry = new Entry(xmin, ymin, xmax, ymax, byteOffset);
entries.add(entry);
if (bounds != null) {
bounds.addPoint(xmin, ymin);
bounds.addPoint(xmax, ymax);
}
}
}
ssx.close();
return entries;
}
/**
* Determines if two rectangles intersect. Actually, this method determines
* if two rectangles don't intersect, and then returns a negation of that
* result. But the bottom line is the same.
*
* @param xmin1 the small x of rectangle 1
* @param ymin1 the small y of rectangle 1
* @param xmax1 the big x of rectangle 1
* @param ymax1 the big y of rectangle 1
* @param xmin2 the small x of rectangle 2
* @param ymin2 the small y of rectangle 2
* @param xmax2 the big x of rectangle 2
* @param ymax2 the big y of rectangle 2
* @return <code>true</code> if the rectangles intersect, <code>false</code>
* if they do not
*/
protected static final boolean intersects(double xmin1, double ymin1, double xmax1,
double ymax1, double xmin2, double ymin2,
double xmax2, double ymax2) {
return !((xmax1 <= xmin2) || (ymax1 <= ymin2) || (xmin1 >= xmax2) || (ymin1 >= ymax2));
}
/**
* Displays the contents of this index.
*
* @param showBounds true to show bounding box, false to skip it
* @exception IOException if something goes wrong reading the file
*/
public void dumpIndex(boolean showBounds) throws IOException {
byte ixRecord[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
int recNum = 0;
if (shpFileName == null) {
return;
}
BinaryBufferedFile ssx = new BinaryBufferedFile(ssx(shpFileName));
ssx.seek(100); // skip the file header
while (true) {
int result = ssx.read(ixRecord, 0, SPATIAL_INDEX_RECORD_LENGTH);
// if (result == -1) {
if (result <= 0) {
logger.info("Processed " + recNum + " records");
break;// EOF
} else {
recNum++;
int offset = readBEInt(ixRecord, 0);
int length = readBEInt(ixRecord, 4);
logger.info("Record "
+ recNum
+ ": "
+ offset
+ ", "
+ length
+ (showBounds ? ("; " + readLEDouble(ixRecord, 8) + ", "
+ readLEDouble(ixRecord, 16) + ", " + readLEDouble(ixRecord, 24)
+ ", " + readLEDouble(ixRecord, 32)) : ""));
}
}
ssx.close();
}
/**
* Prints a usage statement describing how to use this class from the
* command line.
*
* @param out The output stream to use for output
*/
public static void printUsage(PrintStream out) {
String className = SpatialIndex.class.getName();
out.println("Usage:");
out.println();
out.println("java " + className + " -c file.shp");
out.println("Creates spatial index <file.ssx> from " + "shape file <file.shp>.");
out.println();
out.println("java " + className + " -d file.shp");
out.println("Dumps spatial index information, excluding "
+ "bounding boxes to stdout. Useful for " + "comparing to a shape index.");
out.println();
out.println("java " + className + " -d -b file.shp");
out.println("Dumps spatial index information including " + "bounding boxes to stdout.");
out.println();
}
/**
* Locate file 'fileName' in classpath, if it is not an absolute file name.
*
* @return absolute name of the file as a string if found, null otherwise.
*/
public static String locateFile(String name) {
File file = new File(name);
if (file.exists()) {
return name;
} else {
java.net.URL url = ClassLoader.getSystemResource(name);
// OK, now we want to look around for the file, in the
// classpaths, and as a resource. It may be a file in
// a classpath, available for direct access.
if (url != null) {
String newname = url.getFile();
file = new File(newname);
if (file.exists()) {
return newname;
}
}
}
return null;
}
/**
* Create a SpatialIndex object with just a shape file name. If the shape
* file is local, this method will attempt to build the spatial index file
* and place it next to the shape file.
*/
public static SpatialIndex locateAndSetShapeData(String shapeFileName) {
SpatialIndex spi = null;
if (shapeFileName == null) {
return null;
}
int appendixIndex = shapeFileName.indexOf(".shp");
if (logger.isLoggable(Level.FINE)) {
logger.fine("created with just the shape file " + shapeFileName);
}
if (appendixIndex != -1) {
if (BinaryFile.exists(shapeFileName)) {
// OK, the shape files exists - now look for spatial
// index file next to it.
String spatialIndexFileName = ssx(shapeFileName);
// Now, see if the spatialIndexFileName exists, and if
// not, create it.
if (logger.isLoggable(Level.FINE)) {
logger.fine("Trying to locate spatial index file " + spatialIndexFileName);
}
try {
spi = new SpatialIndex(shapeFileName);
} catch (java.io.IOException ioe) {
logger.warning(ioe.getMessage());
ioe.printStackTrace();
spi = null;
}
} else {
logger.warning("Couldn't locate shape file " + shapeFileName);
}
} else {
if (logger.isLoggable(Level.FINE)) {
logger.fine("file " + shapeFileName + " doesn't look like a shape file");
}
}
return spi;
}
/**
* The driver for the command line interface. Reads the command line
* arguments and executes appropriate calls.
* <p>
* See the file documentation for usage.
*
* @param argv the command line arguments
* @exception IOException if something goes wrong reading or writing the
* file
*/
public static void main(String argv[]) throws IOException {
int argc = argv.length;
if (argc == 0) {
// No arguments, give the user some help
printUsage(System.out);
System.exit(0);
}
logger.setLevel(Level.FINER);
if (argv[0].equals("-d")) {
if (argc == 2) {
String name = argv[1];
SpatialIndex si = new SpatialIndex(name);
si.dumpIndex(false);
} else if ((argc == 3) && (argv[1].equals("-b"))) {
String name = argv[2];
SpatialIndex si = new SpatialIndex(name);
si.dumpIndex(true);
} else {
printUsage(System.err);
System.exit(1);
}
} else if ((argc == 2) && argv[0].equals("-c")) {
String shapeFile = argv[1];
SpatialIndex.FileIndex.create(shapeFile);
} else {
printUsage(System.err);
System.exit(1);
}
}
/**
* Set the icon to use for point objects, in general.
*
* @param ii ImageIcon to use for icon.
*/
public synchronized void setPointIcon(ImageIcon ii) {
pointIcon = ii;
}
/**
* Get the icon used for general point objects.
*
* @return ImageIcon, null if not set.
*/
public synchronized ImageIcon getPointIcon() {
return pointIcon;
}
/*
* (non-Javadoc)
*
* @see com.bbn.openmap.io.Closable#close(boolean)
*/
public boolean close(boolean done) {
try {
if (shp != null) {
shp.close();
}
if (done) {
shp = null;
}
if (done && entries != null) {
entries.clear();
entries = null;
}
return true;
} catch (IOException ioe) {
}
return false;
}
/**
* The spatial information for each shp entry is held in one of these.
*
* @author dietrick
*/
public static class Entry {
double xMin;
double yMin;
double xMax;
double yMax;
int byteOffset;
public Entry(double xMin, double yMin, double xMax, double yMax, int byteOffset) {
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
this.byteOffset = byteOffset;
}
public boolean intersects(double xmin, double ymin, double xmax, double ymax) {
return SpatialIndex.intersects(xmin, ymin, xmax, ymax, xMin, yMin, xMax, yMax);
}
public int getByteOffset() {
return byteOffset;
}
public void addToBounds(ESRIBoundingBox bounds) {
bounds.addPoint(xMin, yMin);
bounds.addPoint(xMax, yMax);
}
}
public DbfHandler getDbf() {
return dbf;
}
public void setDbf(DbfHandler dbf) {
this.dbf = dbf;
}
public static class FileIndex {
protected FileIndex() {
}
public static void create(String shpFile) {
FileIndex fi = new FileIndex();
fi.createIndex(shpFile);
}
/**
* Writes the spatial index for a polygon shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param os the spatial index file output stream
*/
protected void indexPolygons(InputStream is, long ptr, OutputStream os) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
byte outBuf[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
int result;
int shapeType;
int nRecords = 0;
int recLengthWords, recLengthBytes /* , recNumber */;
long recOffset;
int recBufSize = 100000;
byte recBuf[] = new byte[recBufSize];
ESRIBoundingBox polyBounds;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Found " + nRecords + " records");
logger.fine("recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
// Null shapes are allowed in any shape file, at any
// time.
shapeType = readLEInt(recBuf, 0);
if (shapeType != SHAPE_TYPE_NULL) {
polyBounds = readBox(recBuf, 4);
} else {
polyBounds = new ESRIBoundingBox();
}
ptr += recLengthBytes + 8;
writeBEInt(outBuf, 0, (int) (recOffset / 2));
writeBEInt(outBuf, 4, recLengthWords);
writeLEDouble(outBuf, 8, polyBounds.min.x);
writeLEDouble(outBuf, 16, polyBounds.min.y);
writeLEDouble(outBuf, 24, polyBounds.max.x);
writeLEDouble(outBuf, 32, polyBounds.max.y);
os.write(outBuf, 0, SPATIAL_INDEX_RECORD_LENGTH);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Writes the spatial index for a point shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param os the spatial index file output stream
*/
protected void indexPoints(InputStream is, long ptr, OutputStream os) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
byte outBuf[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
int result;
int nRecords = 0;
int recLengthWords, recLengthBytes/* , recNumber */;
long recOffset;
int shapeType;
int recBufSize = 20;
byte recBuf[] = new byte[recBufSize];
double x = 0;
double y = 0;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Found " + nRecords + " records");
logger.fine("recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
// Null shapes are allowed in any shape file, at any
// time.
shapeType = readLEInt(recBuf, 0);
if (shapeType != SHAPE_TYPE_NULL) {
x = readLEDouble(recBuf, 4);
y = readLEDouble(recBuf, 12);
}
ptr += recLengthBytes + 8;
writeBEInt(outBuf, 0, (int) (recOffset / 2));
writeBEInt(outBuf, 4, recLengthWords);
writeLEDouble(outBuf, 8, x);
writeLEDouble(outBuf, 16, y);
writeLEDouble(outBuf, 24, x);
writeLEDouble(outBuf, 32, y);
os.write(outBuf, 0, SPATIAL_INDEX_RECORD_LENGTH);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Writes the spatial index for a null shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param os the spatial index file output stream
*/
protected void indexNulls(InputStream is, long ptr, OutputStream os) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
byte outBuf[] = new byte[SPATIAL_INDEX_RECORD_LENGTH];
int result;
int nRecords = 0;
int recLengthWords, recLengthBytes/* , recNumber */;
long recOffset;
int recBufSize = 20;
byte recBuf[] = new byte[recBufSize];
double x;
double y;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Found " + nRecords + " records");
logger.fine("recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
x = 0;
y = 0;
ptr += recLengthBytes + 8;
writeBEInt(outBuf, 0, (int) (recOffset / 2));
writeBEInt(outBuf, 4, recLengthWords);
writeLEDouble(outBuf, 8, x);
writeLEDouble(outBuf, 16, y);
writeLEDouble(outBuf, 24, x);
writeLEDouble(outBuf, 32, y);
os.write(outBuf, 0, SPATIAL_INDEX_RECORD_LENGTH);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Creates a spatial index for a shape file. Reads the records from the
* shape file, writing appropriate index records to the spatial index
* file.
*
* @param inFile the shape file or spatial index file, the method will
* figure it out based on the file name extension.
*/
public void createIndex(String inFile) {
String ssxFile = null;
String shpFile = null;
if (inFile.endsWith(".shp")) {
shpFile = inFile;
ssxFile = ssx(shpFile);
} else if (inFile.endsWith(".ssx")) {
ssxFile = inFile;
shpFile = ssxFile.substring(0, ssxFile.indexOf(".ssx")) + ".shp";
} else {
return;
}
byte fileHeader[] = new byte[SHAPE_FILE_HEADER_LENGTH];
FileInputStream shp = null;
FileOutputStream ssx = null;
int shapeType;
try {
shp = new FileInputStream(shpFile);
ssx = new FileOutputStream(ssxFile);
shp.read(fileHeader, 0, SHAPE_FILE_HEADER_LENGTH);
ssx.write(fileHeader, 0, SHAPE_FILE_HEADER_LENGTH);
shapeType = readLEInt(fileHeader, 32);
switch (shapeType) {
case SHAPE_TYPE_NULL:
indexNulls(shp, SHAPE_FILE_HEADER_LENGTH, ssx);
break;
case SHAPE_TYPE_POINT:
case SHAPE_TYPE_POINTZ:
case SHAPE_TYPE_POINTM:
indexPoints(shp, SHAPE_FILE_HEADER_LENGTH, ssx);
break;
case SHAPE_TYPE_MULTIPOINT:
case SHAPE_TYPE_MULTIPOINTZ:
case SHAPE_TYPE_MULTIPOINTM:
// case SHAPE_TYPE_ARC:
case SHAPE_TYPE_POLYLINE:
case SHAPE_TYPE_POLYLINEZ:
case SHAPE_TYPE_POLYLINEM:
case SHAPE_TYPE_POLYGON:
case SHAPE_TYPE_POLYGONZ:
case SHAPE_TYPE_POLYGONM:
indexPolygons(shp, SHAPE_FILE_HEADER_LENGTH, ssx);
break;
default:
logger.warning("Unknown shape type: " + shapeType);
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
if (shp != null)
shp.close();
if (ssx != null)
ssx.close();
} catch (java.io.IOException e) {
}
}
}
}
public static class MemoryIndex {
protected MemoryIndex() {
}
public static List<Entry> create(String shpFile) {
MemoryIndex mi = new MemoryIndex();
return mi.createIndex(shpFile);
}
/**
* Writes the spatial index for a polygon shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param entries a List of Entries to add to
*/
protected void indexPolygons(InputStream is, long ptr, List<Entry> entries) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
int result;
int shapeType;
int nRecords = 0;
int recLengthWords, recLengthBytes /* , recNumber */;
long recOffset;
int recBufSize = 100000;
byte recBuf[] = new byte[recBufSize];
ESRIBoundingBox polyBounds;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex Found " + nRecords + " records");
logger.fine("Shapefile SpatialIndex recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
// Null shapes are allowed in any shape file, at any
// time.
shapeType = readLEInt(recBuf, 0);
if (shapeType != SHAPE_TYPE_NULL) {
polyBounds = readBox(recBuf, 4);
} else {
polyBounds = new ESRIBoundingBox();
}
ptr += recLengthBytes + 8;
Entry entry = new Entry(polyBounds.min.x, polyBounds.min.y, polyBounds.max.x, polyBounds.max.y, (int) recOffset);
entries.add(entry);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Writes the spatial index for a point shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param entries a List of Entries to add to
*/
protected void indexPoints(InputStream is, long ptr, List<Entry> entries) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
int result;
int nRecords = 0;
int recLengthWords, recLengthBytes/* , recNumber */;
long recOffset;
int shapeType;
int recBufSize = 20;
byte recBuf[] = new byte[recBufSize];
double x = 0;
double y = 0;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Found " + nRecords + " records");
logger.fine("recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
// Null shapes are allowed in any shape file, at any
// time.
shapeType = readLEInt(recBuf, 0);
if (shapeType != SHAPE_TYPE_NULL) {
x = readLEDouble(recBuf, 4);
y = readLEDouble(recBuf, 12);
}
ptr += recLengthBytes + 8;
Entry entry = new Entry(x, y, x, y, (int) recOffset);
entries.add(entry);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Writes the spatial index for a null shape file.
*
* @param is the shape file input stream
* @param ptr the current position in the file
* @param entries a List of Entries to add to
*/
protected void indexNulls(InputStream is, long ptr, List<Entry> entries) {
boolean moreRecords = true;
byte rHdr[] = new byte[SHAPE_RECORD_HEADER_LENGTH];
int result;
int nRecords = 0;
int recLengthWords, recLengthBytes/* , recNumber */;
long recOffset;
int recBufSize = 20;
byte recBuf[] = new byte[recBufSize];
double x;
double y;
try {
while (moreRecords) {
result = is.read(rHdr, 0, SHAPE_RECORD_HEADER_LENGTH);
if (result < 0) {
moreRecords = false;
if (logger.isLoggable(Level.FINE)) {
logger.fine("Found " + nRecords + " records");
logger.fine("recBufSize = " + recBufSize);
}
} else {
nRecords++;
recOffset = ptr;
/* recNumber = */readBEInt(rHdr, 0);
recLengthWords = readBEInt(rHdr, 4);
recLengthBytes = recLengthWords * 2;
if (recLengthBytes > recBufSize) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Shapefile SpatialIndex increasing recBufSize to "
+ recLengthBytes);
}
recBufSize = recLengthBytes;
recBuf = new byte[recBufSize];
}
result = is.read(recBuf, 0, recLengthBytes);
x = 0;
y = 0;
ptr += recLengthBytes + 8;
Entry entry = new Entry(x, y, x, y, (int) recOffset);
entries.add(entry);
}
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
/**
* Creates a spatial index for a shape file. Reads the records from the
* shape file, writing appropriate index records to the spatial index
* file.
*
* @param inFile the shape file.
*/
public List<Entry> createIndex(String inFile) {
String shpFile = null;
List<Entry> entries = new ArrayList<Entry>();
if (inFile.endsWith(".shp")) {
shpFile = inFile;
} else {
logger.warning("can't create spatial index entries from non-shape file: " + inFile);
return entries;
}
byte fileHeader[] = new byte[SHAPE_FILE_HEADER_LENGTH];
BufferedInputStream shp = null;
int shapeType;
try {
URL shpURL = PropUtils.getResourceOrFileOrURL(shpFile);
if (shpURL == null) {
return entries;
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("creating spatial index entries for " + inFile);
}
shp = new BufferedInputStream(shpURL.openStream());
shp.read(fileHeader, 0, SHAPE_FILE_HEADER_LENGTH);
shapeType = readLEInt(fileHeader, 32);
switch (shapeType) {
case SHAPE_TYPE_NULL:
indexNulls(shp, SHAPE_FILE_HEADER_LENGTH, entries);
break;
case SHAPE_TYPE_POINT:
case SHAPE_TYPE_POINTZ:
case SHAPE_TYPE_POINTM:
indexPoints(shp, SHAPE_FILE_HEADER_LENGTH, entries);
break;
case SHAPE_TYPE_MULTIPOINT:
case SHAPE_TYPE_MULTIPOINTZ:
case SHAPE_TYPE_MULTIPOINTM:
// case SHAPE_TYPE_ARC:
case SHAPE_TYPE_POLYLINE:
case SHAPE_TYPE_POLYLINEZ:
case SHAPE_TYPE_POLYLINEM:
case SHAPE_TYPE_POLYGON:
case SHAPE_TYPE_POLYGONZ:
case SHAPE_TYPE_POLYGONM:
indexPolygons(shp, SHAPE_FILE_HEADER_LENGTH, entries);
break;
default:
logger.warning("Unknown shape type: " + shapeType);
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
try {
if (shp != null)
shp.close();
} catch (java.io.IOException e) {
}
}
return entries;
}
}
}