Skip to content
Browse files

Merge pull request #763 from ahuarte47/fast_query_dbase-windows

[GEOS-6842] Improve the dbase reader to filter WHERE clauses using an ODBC driver
  • Loading branch information...
2 parents 10e8ebe + 8773658 commit 9e915ff5319a8fb90ad18e7940911345f78587fb @aaime aaime committed
View
5 modules/plugin/shapefile/pom.xml
@@ -143,6 +143,11 @@
<artifactId>gt-data</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.geotools</groupId>
+ <artifactId>gt-jdbc</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.jdom</groupId>
View
78 modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/RecnoFilterToSQL.java
@@ -0,0 +1,78 @@
+/*
+ * GeoTools - The Open Source Java GIS Toolkit
+ * http://geotools.org
+ *
+ * (C) 2002-2015, Open Source Geospatial Foundation (OSGeo)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ */
+package org.geotools.data.shapefile;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.geotools.data.jdbc.FilterToSQL;
+import org.geotools.filter.FilterCapabilities;
+
+/**
+ * Encodes a filter into a SQL WHERE statement for the JDBC providers used with the optional RECNO field index.
+ *
+ * @author Alvaro Huarte - Tracasa / ahuarte@tracasa.es
+ */
+public class RecnoFilterToSQL extends FilterToSQL
+{
+ public RecnoFilterToSQL(FilterCapabilities filterCapabilities)
+ {
+ this.filterCapabilities = filterCapabilities;
+ }
+
+ @Override
+ protected FilterCapabilities createFilterCapabilities()
+ {
+ FilterCapabilities caps = new FilterCapabilities();
+ caps.addAll(filterCapabilities);
+ return caps;
+ }
+ private FilterCapabilities filterCapabilities;
+
+ static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+ static {
+ // Set DATE_FORMAT time zone to GMT, as Date's are always in GMT internaly. Otherwise we'll
+ // get a local timezone encoding regardless of the actual Date value
+ DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
+ }
+ static SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ @Override
+ protected void writeLiteral(Object literal) throws IOException
+ {
+ if (literal instanceof Date)
+ {
+ out.write("'");
+
+ if (literal instanceof java.sql.Date)
+ {
+ out.write(DATE_FORMAT.format(literal));
+ }
+ else
+ {
+ out.write(DATETIME_FORMAT.format(literal));
+ }
+ out.write("'");
+ }
+ else
+ {
+ super.writeLiteral(literal);
+ }
+ }
+}
View
310 modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/RecnoIndexManager.java
@@ -0,0 +1,310 @@
+/*
+ * GeoTools - The Open Source Java GIS Toolkit
+ * http://geotools.org
+ *
+ * (C) 2002-2015, Open Source Geospatial Foundation (OSGeo)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ */
+package org.geotools.data.shapefile;
+
+import static org.geotools.data.shapefile.files.ShpFileType.SHP;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.logging.Logger;
+
+import org.geotools.util.logging.Logging;
+import org.geotools.data.shapefile.index.CloseableIterator;
+import org.geotools.data.shapefile.index.Data;
+import org.geotools.data.shapefile.index.DataDefinition;
+import org.geotools.data.shapefile.shp.IndexFile;
+import org.geotools.data.jdbc.FilterToSQLException;
+
+import org.geotools.filter.FilterCapabilities;
+import org.opengis.filter.ExcludeFilter;
+import org.opengis.filter.Filter;
+import org.opengis.filter.IncludeFilter;
+import org.opengis.filter.PropertyIsLike;
+import org.opengis.filter.PropertyIsNull;
+import org.opengis.filter.expression.Add;
+import org.opengis.filter.expression.Divide;
+import org.opengis.filter.expression.Multiply;
+import org.opengis.filter.expression.Subtract;
+
+/**
+ * Manages the optional RECNO field index on behalf of the {@link ShapefileDataStore}
+ * It only works for Windows SO's.
+ *
+ * @author Alvaro Huarte - Tracasa / ahuarte@tracasa.es
+ */
+public class RecnoIndexManager
+{
+ static final Logger LOGGER = Logging.getLogger(RecnoIndexManager.class);
+
+ // The Microsoft Visual FoxPro Driver is available!
+ static boolean MICROSOFT_FOXPRO_DRIVER_INSTALLED = false;
+ // The Advantage StreamlineSQL ODBC is available!
+ static boolean ADVANTAGE_ODBC_DRIVER_INSTALLED = false;
+
+ // Describes the allowed filters we support for alphanumeric Dbase queries.
+ static final FilterCapabilities filterCapabilities = new FilterCapabilities();
+
+ // Static constructor of RecnoIndexManager class.
+ static
+ {
+ boolean runningWindows = System.getProperty("os.name").toUpperCase().contains("WINDOWS");
+
+ /* TODO: Now, it only works for two ODBC drivers running in Windows SO's:
+ * - Microsoft ODBC FoxPro Driver (x86).
+ * - Advantage StreamlineSQL ODBC driver (x86/x64).
+ *
+ * It is feasible use the 'Advantage StreamlineSQL ODBC' in Linux platforms.
+ * See...
+ * http://devzone.advantagedatabase.com/dz/content.aspx?Key=20&Release=16&Product=14
+ * http://scn.sap.com/docs/DOC-39207
+ */
+ if (runningWindows)
+ {
+ java.sql.Connection connection = null;
+
+ String connectionString = null;
+ String tablePath = System.getProperty("user.dir");
+
+ // Get if available two 'superfast' JDBC driver of Windows for DBF tables.
+ // 1) Microsoft Visual FoxPro Driver:
+ try
+ {
+ Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
+ connectionString = "jdbc:odbc:Driver={Microsoft Visual FoxPro Driver};SourceType=DBF;SourceDB="+tablePath+";";
+
+ if ((connection = java.sql.DriverManager.getConnection(connectionString, "", ""))!=null)
+ {
+ MICROSOFT_FOXPRO_DRIVER_INSTALLED = true;
+ connection.close();
+
+ LOGGER.info("The 'Microsoft Visual FoxPro Driver' is available!");
+ }
+ }
+ catch (Exception error)
+ {
+ LOGGER.info("The 'Microsoft Visual FoxPro Driver' is not available!");
+ }
+ // 2) Advantage StreamlineSQL ODBC Driver:
+ try
+ {
+ Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
+ connectionString = "jdbc:odbc:Driver={Advantage StreamlineSQL ODBC};DataDirectory="+tablePath+";DefaultType=FoxPro;ServerTypes=1;AdvantageLocking=OFF;Pooling=FALSE;ShowDeleted=FALSE;";
+
+ if ((connection = java.sql.DriverManager.getConnection(connectionString, "", ""))!=null)
+ {
+ ADVANTAGE_ODBC_DRIVER_INSTALLED = true;
+ connection.close();
+
+ LOGGER.info("The 'Advantage StreamlineSQL ODBC Driver' is available!");
+ }
+ }
+ catch (Exception error)
+ {
+ LOGGER.info("The 'Advantage StreamlineSQL ODBC Driver' is not available!");
+ }
+
+ // Common alphanumeric filter capabilities of the JDBC providers.
+ filterCapabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS);
+ filterCapabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
+ filterCapabilities.addType(Add.class);
+ filterCapabilities.addType(Subtract.class);
+ filterCapabilities.addType(Multiply.class);
+ filterCapabilities.addType(Divide.class);
+ filterCapabilities.addType(PropertyIsNull.class);
+ filterCapabilities.addType(IncludeFilter.class);
+ filterCapabilities.addType(ExcludeFilter.class);
+ filterCapabilities.addType(PropertyIsLike.class);
+ }
+ }
+
+ /**
+ * Returns the record index collection that matches with the specified filter using one super fast ODBC Driver.
+ */
+ private static List<Integer> queryRecnoIndex(String shapeFileName, Filter filter, int maxFeatures) throws SQLException, ClassNotFoundException, IOException, FilterToSQLException
+ {
+ java.sql.Connection connection = null;
+ java.sql.Statement stmt = null;
+ java.sql.ResultSet rs = null;
+
+ List<Integer> recnoList = new ArrayList<Integer>();
+ try
+ {
+ File file = org.geotools.data.DataUtilities.urlToFile(new java.net.URL(shapeFileName));
+ if (file==null) return null;
+
+ Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
+ String connectionString = null;
+ String tablePath = file.getParentFile().getPath();
+ String tableName = file.getName().substring(0, file.getName().lastIndexOf("."));
+ String whereFilter = new RecnoFilterToSQL(filterCapabilities).encodeToString(filter);
+
+ // TableName is valid ?
+ if (tableName.indexOf('-')!=-1 || tableName.indexOf('(')!=-1)
+ return null;
+
+ if (ADVANTAGE_ODBC_DRIVER_INSTALLED)
+ {
+ connectionString = "jdbc:odbc:Driver={Advantage StreamlineSQL ODBC};DataDirectory="+tablePath+";DefaultType=FoxPro;ServerTypes=1;AdvantageLocking=OFF;Pooling=FALSE;ShowDeleted=FALSE;";
+
+ // Read the Fid's from the specified Query with this Driver.
+ if ((connection = java.sql.DriverManager.getConnection(connectionString, "", ""))!=null)
+ {
+ if ((stmt = connection.createStatement())!=null)
+ {
+ String sql = maxFeatures!=-1 && maxFeatures<Integer.MAX_VALUE ?
+ String.format("SELECT TOP %d ROWID FROM [%s] %s;", maxFeatures, tableName, whereFilter) :
+ String.format("SELECT ROWID FROM [%s] %s;", tableName, whereFilter);
+
+ if ((rs = stmt.executeQuery(sql))!=null)
+ {
+ while (rs.next())
+ {
+ Integer id = RecnoIndexManager.ConvertRowidToRecno(rs.getString(1));
+ recnoList.add(id - 1);
+ }
+ }
+ }
+ }
+ }
+ else
+ if (MICROSOFT_FOXPRO_DRIVER_INSTALLED)
+ {
+ connectionString = "jdbc:odbc:Driver={Microsoft Visual FoxPro Driver};SourceType=DBF;SourceDB="+tablePath+";";
+
+ // Read the Fid's from the specified Query with this Driver.
+ if ((connection = java.sql.DriverManager.getConnection(connectionString, "", ""))!=null)
+ {
+ if ((stmt = connection.createStatement())!=null)
+ {
+ String sql = maxFeatures!=-1 && maxFeatures<Integer.MAX_VALUE ?
+ String.format("SELECT TOP %d recno() FROM [%s] %s;", maxFeatures, tableName, whereFilter) :
+ String.format("SELECT recno() FROM [%s] %s;", tableName, whereFilter);
+
+ if ((rs = stmt.executeQuery(sql))!=null)
+ {
+ while (rs.next())
+ {
+ Integer id = rs.getInt(1);
+ recnoList.add(id - 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ org.geotools.data.jdbc.JDBCUtils.close(rs);
+ org.geotools.data.jdbc.JDBCUtils.close(stmt);
+ org.geotools.data.jdbc.JDBCUtils.close(connection, null, null);
+ }
+ return recnoList;
+ }
+
+ /**
+ * Convert the specified Advantage StreamlineSQL ROWID to the DBF RECNO value.
+ */
+ private static int ConvertRowidToRecno(String rowID)
+ {
+ final String BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ // The RecNo is the last 6 characters of the ROWID.
+ int recno = 0;
+ recno += BASE64.indexOf(rowID.charAt(12)) * 1073741824;
+ recno += BASE64.indexOf(rowID.charAt(13)) * 16777216;
+ recno += BASE64.indexOf(rowID.charAt(14)) * 262144;
+ recno += BASE64.indexOf(rowID.charAt(15)) * 4096;
+ recno += BASE64.indexOf(rowID.charAt(16)) * 64;
+ recno += BASE64.indexOf(rowID.charAt(17));
+ return recno;
+ }
+
+ /**
+ * Uses the optional Recno field to quickly lookup the shp offset and the record number for the list of fids.
+ * Now it only works for two ODBC drivers running in Windows SO's:
+ * - Microsoft ODBC FoxPro Driver (x86).
+ * - Advantage StreamlineSQL ODBC driver (x86/x64).
+ *
+ * @todo It is feasible use the 'Advantage StreamlineSQL ODBC' in Linux platforms.
+ */
+ public static CloseableIterator<Data> queryRecnoIndex(ShapefileDataStore featureStore, Filter filter, int maxFeatures, CloseableIterator<Data> goodRecs) throws SQLException, ClassNotFoundException, IOException, FilterToSQLException
+ {
+ if ((MICROSOFT_FOXPRO_DRIVER_INSTALLED || ADVANTAGE_ODBC_DRIVER_INSTALLED) && filter!=null && !Filter.INCLUDE.equals(filter) && !Filter.EXCLUDE.equals(filter) && filterCapabilities.fullySupports(filter))
+ {
+ String shapeFileName = featureStore.shpFiles.get(SHP);
+
+ List<Integer> recnoList = RecnoIndexManager.queryRecnoIndex(shapeFileName, filter, maxFeatures);
+ List<Data> records = new ArrayList<Data>();
+ if (recnoList==null) return goodRecs;
+
+ if (recnoList.size()>0)
+ {
+ IndexFile shx = featureStore.shpManager.openIndexFile();
+
+ try
+ {
+ DataDefinition def = new DataDefinition("US-ASCII");
+ def.addField(Integer.class);
+ def.addField(Long.class);
+
+ // Filter the already good records from a previous spatial indexing.
+ if (goodRecs!=null)
+ {
+ HashMap<Integer,Integer> recnoHash = new HashMap<Integer,Integer>();
+
+ for (int i = 0, icount = recnoList.size(); i < icount; i++)
+ {
+ int recno = recnoList.get(i);
+ recnoHash.put(recno+1,recno);
+ }
+ while (goodRecs.hasNext())
+ {
+ Data data = goodRecs.next();
+ if (recnoHash.containsKey(data.getValue(0))) records.add(data);
+ }
+ recnoHash.clear();
+ goodRecs.close();
+ }
+ else
+ {
+ for (int i = 0, icount = recnoList.size(); i < icount; i++)
+ {
+ int recno = recnoList.get(i);
+
+ Data data = new Data(def);
+ data.addValue(new Integer(recno + 1));
+ data.addValue(new Long(shx.getOffsetInBytes(recno)));
+
+ records.add(data);
+ }
+ }
+ recnoList.clear();
+ }
+ finally
+ {
+ shx.close();
+ }
+ }
+ return new CloseableIteratorWrapper<Data>(records.iterator());
+ }
+ return goodRecs;
+ }
+}
View
14 modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/ShapefileDataStore.java
@@ -109,6 +109,8 @@
boolean indexCreationEnabled = true;
+ boolean odbcFilteringEnabled = true;
+
boolean fidIndexed = true;
IndexManager indexManager;
@@ -440,7 +442,7 @@ public void setFidIndexed(boolean fidIndexed) {
public String toString() {
return "ShapefileDataStore [file=" + shpFiles.get(SHP) + ", charset=" + charset + ", timeZone=" + timeZone
+ ", memoryMapped=" + memoryMapped + ", bufferCachingEnabled="
- + bufferCachingEnabled + ", indexed=" + indexed + ", fidIndexed=" + fidIndexed
+ + bufferCachingEnabled + ", indexed=" + indexed + ", fidIndexed=" + fidIndexed + ", odbcFilteringEnabled=" + odbcFilteringEnabled
+ "]";
}
@@ -479,6 +481,16 @@ public void setIndexCreationEnabled(boolean indexCreationEnabled) {
this.indexCreationEnabled = indexCreationEnabled;
}
+ public boolean isOdbcFilteringEnabled() {
+ return odbcFilteringEnabled;
+ }
+
+ /**
+ * If true (default) the store uses an available JDBC provider to execute the Dbase filters
+ */
+ public void setOdbcFilteringEnabled(boolean odbcFilteringEnabled) {
+ this.odbcFilteringEnabled = odbcFilteringEnabled;
+ }
}
View
24 ...plugin/shapefile/src/main/java/org/geotools/data/shapefile/ShapefileDataStoreFactory.java
@@ -91,6 +91,22 @@
new KVP(Param.LEVEL, "advanced"));
/**
+ * Optional - Enable/disable the automatic use of the optional Recno field from the DBF file
+ * to quickly execute Dbase filters.
+ *
+ * The FeatureStore previously checks if there is an avaliable a ODBC provider in the system
+ * and compatible with the use of Recno fields of DBF files.
+ *
+ * Now it only works for two ODBC drivers running in Windows SO's:
+ * - Microsoft ODBC FoxPro Driver (x86).
+ * - Advantage StreamlineSQL ODBC driver (x86/x64).
+ * It is feasible implement this using the 'Advantage StreamlineSQL ODBC' driver in Linux platforms.
+ */
+ public static final Param USE_ODBC_DBASE_FILTERING = new Param("use a ODBC provider to fast execution of Dbase filters",
+ Boolean.class, "enable/disable the automatic use of an available ODBC provider to fast execution of Dbase filters", false, true,
+ new KVP(Param.LEVEL, "advanced"));
+
+ /**
* Optional - character used to decode strings from the DBF file
*/
public static final Param DBFCHARSET = new Param("charset", Charset.class,
@@ -148,7 +164,7 @@ public String getDescription() {
}
public Param[] getParametersInfo() {
- return new Param[] { URLP, NAMESPACEP, ENABLE_SPATIAL_INDEX, CREATE_SPATIAL_INDEX, DBFCHARSET, DBFTIMEZONE,
+ return new Param[] { URLP, NAMESPACEP, ENABLE_SPATIAL_INDEX, CREATE_SPATIAL_INDEX, USE_ODBC_DBASE_FILTERING, DBFCHARSET, DBFTIMEZONE,
MEMORY_MAPPED, CACHE_MEMORY_MAPS, FILE_TYPE, FSTYPE };
}
@@ -173,6 +189,11 @@ public DataStore createDataStore(Map<String, Serializable> params) throws IOExce
// should not be needed as default is TRUE
isEnableSpatialIndex = Boolean.TRUE;
}
+ Boolean isEnableOdbcFiltering = (Boolean) USE_ODBC_DBASE_FILTERING.lookUp(params);
+ if (isEnableOdbcFiltering == null) {
+ // should not be needed as default is TRUE
+ isEnableOdbcFiltering = true;
+ }
// are we creating a directory of shapefiles store, or a single one?
File dir = DataUtilities.urlToFile(url);
@@ -198,6 +219,7 @@ public DataStore createDataStore(Map<String, Serializable> params) throws IOExce
store.setTimeZone(dbfTimeZone);
store.setIndexed(enableIndex);
store.setIndexCreationEnabled(createIndex);
+ store.setOdbcFilteringEnabled(isEnableOdbcFiltering);
return store;
}
}
View
21 ...es/plugin/shapefile/src/main/java/org/geotools/data/shapefile/ShapefileFeatureSource.java
@@ -322,6 +322,9 @@ protected int getCountInternal(Query query) throws IOException {
bbox = (Envelope) q.getFilter().accept(ExtractBoundsFilterVisitor.BOUNDS_VISITOR, bbox);
}
+ boolean usingBoundingBox = false;
+ boolean usingFidIndex = false;
+
// see if we can use indexing to speedup the data access
Filter filter = q != null ? q.getFilter() : null;
IndexManager indexManager = getDataStore().indexManager;
@@ -331,12 +334,14 @@ protected int getCountInternal(Query query) throws IOException {
List<Data> records = indexManager.queryFidIndex(fidFilter);
if (records != null) {
goodRecs = new CloseableIteratorWrapper<Data>(records.iterator());
+ usingFidIndex = true;
}
} else if (getDataStore().isIndexed() && !bbox.isNull()
&& !Double.isInfinite(bbox.getWidth()) && !Double.isInfinite(bbox.getHeight())) {
try {
if(indexManager.isSpatialIndexAvailable() || getDataStore().isIndexCreationEnabled()) {
goodRecs = indexManager.querySpatialIndex(bbox);
+ usingBoundingBox = true;
}
} catch (TreeException e) {
throw new IOException("Error querying index: " + e.getMessage());
@@ -350,6 +355,22 @@ protected int getCountInternal(Query query) throws IOException {
return new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(resultSchema);
}
+ // uses the optional Recno field to quickly lookup the shp offset and the record number.
+ if (!usingBoundingBox && !usingFidIndex && getDataStore().isOdbcFilteringEnabled()) {
+ try {
+ goodRecs = RecnoIndexManager.queryRecnoIndex(getDataStore(), filter, q.getMaxFeatures(), goodRecs);
+
+ // do we have anything to read at all? If not don't bother opening all the files
+ if (goodRecs!=null && !goodRecs.hasNext()) {
+ LOGGER.log(Level.FINE, "Empty results for " + resultSchema.getName().getLocalPart() + ", skipping read");
+ goodRecs.close();
+ return new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(resultSchema);
+ }
+ } catch (Exception error) {
+ LOGGER.log(Level.WARNING, "Error querying renco field. msg='" + error.getMessage() + "'.");
+ }
+ }
+
// get the .fix file reader, if we have a .fix file
IndexedFidReader fidReader = null;
if (getDataStore().isFidIndexed() && filter instanceof Id && indexManager.hasFidIndex(false)) {
View
20 ...es/plugin/shapefile/src/test/java/org/geotools/data/shapefile/ShapefileDataStoreTest.java
@@ -455,6 +455,26 @@ public void testQueryBboxNonGeomAttributes() throws Exception {
}
@Test
+ public void testQueryNonGeomAttributes() throws Exception {
+ File shpFile = copyShapefiles(STATE_POP);
+ URL url = shpFile.toURI().toURL();
+ ShapefileDataStore ds = new ShapefileDataStore(url);
+ SimpleFeatureSource fs = ds.getFeatureSource();
+
+ // GEOS-6842/GEOT-4991:
+ // Build an alphanumeric query to use optionally an ODBC provider to execute fast Dbase filters.
+ FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
+ Filter filter = ff.equals(ff.property("STATE_ABBR"), ff.literal("AL"));
+ Query q = new Query();
+ q.setFilter(filter);
+
+ // grab the features
+ SimpleFeatureCollection fc = fs.getFeatures(q);
+ assertTrue(fc.size() > 0);
+ ds.dispose();
+ }
+
+ @Test
public void testFidFilter() throws Exception {
File shpFile = copyShapefiles(STATE_POP);
URL url = shpFile.toURI().toURL();

0 comments on commit 9e915ff

Please sign in to comment.
Something went wrong with that request. Please try again.