/* * ShapeGeometryImporter.java * * Created on June 9, 2006, 1:51 PM * */ package ika.geoimport; import ika.geo.*; import ika.utils.MixedEndianDataInputStream; import java.io.*; /** * An importer for ESRI shape files. This importer only reads geometry from * .shp files. * @author Bernhard Jenny, Institute of Cartography, ETH Zurich. */ public class ShapeGeometryImporter extends GeoImporter { /** Identifiers for different shape types. */ private static final int NULLSHAPE = 0; private static final int POINT = 1; private static final int POLYLINE = 3; private static final int POLYGON = 5; private static final int MULTIPOINT = 8; private static final int POINTZ = 11; private static final int POLYLINEZ = 13; private static final int POLYGONZ = 15; private static final int MULTIPOINTZ = 18; private static final int POINTM = 21; private static final int POLYLINEM = 23; private static final int POLYGONM = 25; private static final int MULTIPOINTM = 28; private static final int MULTIPATCH = 31; // not supported yet /** * ESRI shapefile magic code at the beginning of the .shp file. */ private static final int FILE_CODE = 9994; /** * Creates a new instance of ShapeGeometryImporter */ public ShapeGeometryImporter() { } protected java.net.URL findDataURL(java.net.URL url) { if (url == null || url.getPath().length() < 5) { return null; } String dataFileExtension = this.getLowerCaseDataFileExtension(); String lowerCaseFilePath = url.getPath().toLowerCase(); if (lowerCaseFilePath.endsWith("." + dataFileExtension)) { return url; } final boolean is_shp_sibling = lowerCaseFilePath.endsWith(".dbf") || lowerCaseFilePath.endsWith(".prj") || lowerCaseFilePath.endsWith(".sbn") || lowerCaseFilePath.endsWith(".sbx") || lowerCaseFilePath.endsWith(".shx"); if (!is_shp_sibling) { return null; } url = ika.utils.URLUtils.replaceFileExtension(url, dataFileExtension); if (ika.utils.URLUtils.resourceExists(url)) { return url; } url = ika.utils.URLUtils.replaceFileExtension(url, dataFileExtension.toUpperCase()); return ika.utils.URLUtils.resourceExists(url) ? url : null; } protected String getLowerCaseDataFileExtension() { return "shp"; } private java.net.URL findSHXURL(java.net.URL url) { if (url == null || url.getPath().length() < 5) { return null; } url = ika.utils.URLUtils.replaceFileExtension(url, "shx"); if (!ika.utils.URLUtils.resourceExists(url)) { url = ika.utils.URLUtils.replaceFileExtension(url, "SHX"); } return ika.utils.URLUtils.resourceExists(url) ? url : null; } protected BufferedInputStream findInputStream(java.net.URL url) throws IOException { BufferedInputStream bis = new BufferedInputStream(url.openStream()); return bis; } protected GeoObject importData(java.net.URL url) throws IOException { MixedEndianDataInputStream is = null; try { url = this.findDataURL(url); if (url == null) { return null; } GeoSet geoSet = this.createGeoSet(); geoSet.setName(ika.utils.FileUtils.getFileNameWithoutExtension(url.getPath())); BufferedInputStream bis = this.findInputStream(url); is = new MixedEndianDataInputStream(bis); // magic code is 9994 int fileCode = is.readInt(); if (fileCode != FILE_CODE) { throw new IOException("File is not an ESRI Shape file. " + "Found file code: " + fileCode); } is.skipBytes(5 * 4); int fileLength = is.readInt() * 2; // read version and shape type int version = is.readLittleEndianInt(); int shapeType = is.readLittleEndianInt(); // skip bounding box and four double values is.skipBytes(8 * 8); // Read all features stored in records. The shp file does not contain // the number of records present in the file. The shx file can be // used to extract this information. // If the shx file cannot be found, -1 is returned. final int recordCount = this.readRecordCountFromSHXFile(url); final int[] recOffsets = readSHXFile(url); // Read until as many records as specified in the shx file are // imported or until the end of file is reached and an EOFException // is thrown. int currentRecord = 0; int currentPos = 100; try { while (true) { // move to beginning of record is.skipBytes(recOffsets[currentRecord] - currentPos); currentPos = recOffsets[currentRecord]; currentPos += readRecord(is, geoSet); if (progressIndicator != null) { final int percentage = (currentRecord + 1) * 100 / recordCount; if (!progressIndicator.progress(percentage)) { return null; } } if (++currentRecord == recordCount) { break; } } } catch (EOFException e) { // EOFException indicates that all records have been read. } // setup the symbol VectorSymbol symbol = new VectorSymbol(); symbol.setScaleInvariant(true); symbol.setStrokeWidth(1); if (shapeType == POLYGON || shapeType == POLYGONZ || shapeType == POLYGONM) { symbol.setFilled(true); symbol.setFillColor(java.awt.Color.WHITE); } geoSet.setVectorSymbol(symbol); return geoSet; } finally { if (is != null) { is.close(); } } } public String getImporterName() { return "Shape Importer"; } private int readRecord(MixedEndianDataInputStream is, GeoSet geoSet) throws IOException { final int recordNumber = is.readInt(); final int contentLength = is.readInt() * 2; // content is at least one int (i.e. the ShapeType) if (contentLength < 4) { throw new EOFException("Negative record length"); } final int shapeType = is.readLittleEndianInt(); int recordBytesRead = 8 + 4; // header is 8 bytes, shapeType is 4 bytes switch (shapeType) { case NULLSHAPE: break; case POINT: case POINTZ: case POINTM: recordBytesRead += readPoint(is, geoSet, recordNumber); break; case MULTIPOINT: case MULTIPOINTZ: case MULTIPOINTM: recordBytesRead += readMultipoint(is, geoSet, recordNumber); break; case POLYLINE: case POLYLINEZ: case POLYLINEM: recordBytesRead += readPolyline(is, geoSet, recordNumber); break; case POLYGON: case POLYGONZ: case POLYGONM: recordBytesRead += readPolygon(is, geoSet, recordNumber); break; case MULTIPATCH: throw new IOException("Multipatch Shape files are not supported."); default: throw new IOException("Shapefile contains unsupported " + "geometry type: " + shapeType); } return recordBytesRead; } private int readPoint(MixedEndianDataInputStream is, GeoSet geoSet, int recordID) throws IOException { final double x = is.readLittleEndianDouble(); final double y = is.readLittleEndianDouble(); GeoPoint geoPoint = new GeoPoint(x, y); geoSet.add(geoPoint); geoSet.setID(recordID); return 2 * 8; } private int readMultipoint(MixedEndianDataInputStream is, GeoSet geoSet, int recordID) throws IOException { is.skipBytes(4 * 8); // skip bounding box final int numPoints = is.readLittleEndianInt(); for (int ptID = 0; ptID < numPoints; ptID++) { readPoint(is, geoSet, recordID); } return 4 * 8 + 4 + numPoints * 2 * 8; } private int readPolyline(MixedEndianDataInputStream is, GeoSet geoSet, int recordID) throws IOException { is.skipBytes(4 * 8); // skip bounding box final int numParts = is.readLittleEndianInt(); final int numPoints = is.readLittleEndianInt(); // read indices into point array int[] pointIds = new int[numParts]; for (int partID = 0; partID < numParts; partID++) { pointIds[partID] = is.readLittleEndianInt(); } // read point array double[] x = new double[numPoints]; double[] y = new double[numPoints]; for (int ptID = 0; ptID < numPoints; ptID++) { x[ptID] = is.readLittleEndianDouble(); y[ptID] = is.readLittleEndianDouble(); } // construct one GeoPath GeoPath geoPath = this.createGeoPath(); geoPath.setID(recordID); for (int partID = 0; partID < numParts; partID++) { int firstPtID = pointIds[partID]; int lastPtID = partID + 1 < numParts ? pointIds[partID + 1] : numPoints; // part must have at least two points if ((lastPtID - firstPtID) < 2) { continue; } geoPath.moveTo(x[firstPtID], y[firstPtID]); for (int ptID = firstPtID + 1; ptID < lastPtID; ptID++) { geoPath.lineTo(x[ptID], y[ptID]); } } geoSet.add(geoPath); return 4 * 8 + 4 + 4 + numParts * 4 + numPoints * 2 * 8; } private int readPolygon(MixedEndianDataInputStream is, GeoSet geoSet, int recordID) throws IOException { is.skipBytes(4 * 8); // skip bounding box final int numParts = is.readLittleEndianInt(); final int numPoints = is.readLittleEndianInt(); // read indices into point array int[] pointIds = new int[numParts]; for (int partID = 0; partID < numParts; partID++) { pointIds[partID] = is.readLittleEndianInt(); } // read point array double[] x = new double[numPoints]; double[] y = new double[numPoints]; for (int ptID = 0; ptID < numPoints; ptID++) { x[ptID] = is.readLittleEndianDouble(); y[ptID] = is.readLittleEndianDouble(); } // construct one GeoPath GeoPath geoPath = this.createGeoPath(); geoPath.setID(recordID); // add sections for (int partID = 0; partID < numParts; partID++) { int firstPtID = pointIds[partID]; int lastPtID = partID + 1 < numParts ? pointIds[partID + 1] : numPoints - 1; // part must have at least two points if ((lastPtID - firstPtID) < 2) { continue; } geoPath.moveTo(x[firstPtID], y[firstPtID]); for (int ptID = firstPtID + 1; ptID < lastPtID; ptID++) { geoPath.lineTo(x[ptID], y[ptID]); } // close polygon when there are more than 2 points if (geoPath.getDrawingInstructionCount() > 2) { geoPath.closePath(); } } geoSet.add(geoPath); return 4 * 8 + 4 + 4 + numParts * 4 + numPoints * 2 * 8; } /** * Reads the number of records from the shx file. * @param shapeURL The URL of the data shape file. * @return The number of records or -1 if the shx file cannot be found. */ private int readRecordCountFromSHXFile(java.net.URL shapeURL) { MixedEndianDataInputStream is = null; try { java.net.URL shxURL = findSHXURL(shapeURL); if (shxURL == null) { return -1; } BufferedInputStream bis = new BufferedInputStream(shxURL.openStream()); is = new MixedEndianDataInputStream(bis); is.skipBytes(24); final int fileLength = is.readInt() * 2; final int recordsCount = (fileLength - 100) / 8; return recordsCount; } catch (java.io.IOException e) { return -1; } finally { if (is != null) { try { is.close(); } catch (java.io.IOException e) { } } } } private int[] readSHXFile(java.net.URL shapeURL) { MixedEndianDataInputStream is = null; try { java.net.URL shxURL = findSHXURL(shapeURL); if (shxURL == null) { return null; } BufferedInputStream bis = new BufferedInputStream(shxURL.openStream()); is = new MixedEndianDataInputStream(bis); is.skipBytes(24); final int fileLength = is.readInt() * 2; final int recordsCount = (fileLength - 100) / 8; int[] offsets = new int[recordsCount]; is.skipBytes(72); int[] recOffsets = new int[recordsCount]; for (int i = 0; i < recordsCount; i++) { recOffsets[i] = is.readInt() * 2; // skip length is.readInt(); /* System.out.println("Shape " + i); System.out.println("Offset: " + is.readInt() * 2); System.out.println("Content Length: " + is.readInt() * 2); */ } return recOffsets; } catch (java.io.IOException e) { return null; } finally { if (is != null) { try { is.close(); } catch (java.io.IOException e) { } } } } }