1. Overview
In this article, we’ll go through the basics of the GeoTools open source Java library – for working with geospatial data. This library provides compliant methods for implementing Geographic Information Systems (GIS) and implements and supports many Open Geospatial Consortium (OGC) standards.
As the OGC develops new standards, they’re implemented by the GeoTools, which makes it quite handy for geospatial work.
2. Dependencies
We’ll need to add the GeoTools dependencies to our pom.xml file. Since these dependencies are not hosted on Maven Central, we also need to declare their repositories so that Maven can download them:
<repositories> <repository> <id>osgeo</id> <name>Open Source Geospatial Foundation Repository</name> <url>http://download.osgeo.org/webdav/geotools/</url> </repository> <repository> <id>opengeo</id> <name>OpenGeo Maven Repository</name> <url>http://repo.opengeo.org</url> </repository> </repositories>
After that, we can add our dependencies:
<dependency> <groupId>org.geotools</groupId> <artifactId>gt-shapefile</artifactId> <version>15.2</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-epsg-hsql</artifactId> <version>15.2</version> </dependency>
3. GIS And Shapefiles
To have any practical use of the GeoTools library, we’ll need to know a few things about geographic information systems and shapefiles.
3.1. GIS
If we want to work with geographical data, we’ll need a geographic information system (GIS). This system can be used to present, capture, store, manipulate, analyze, or manage geographical data.
Some portion of the geographical data is spatial – it references concrete locations on earth. The spatial data is usually accompanied by the attribute data. Attribute data can be any additional information about each of the spatial features.
An example of geographical data would be cities. The actual location of the cities is the spatial data. Additional data such as the city name and population would make up the attribute data.
3.2. Shapefiles
Different formats are available for working with geospatial data. Raster and vector are the two primary data types.
In this article, we’re going to see how to work with the vector data type. This data type can be represented as points, lines, or polygons.
To store vector data in a file, we will use a shapefile. This file format is used when working with the geospatial vector data type. Also, it is compatible with a wide range of GIS software.
We can use GeoTools to add features like cities, schools, and landmarks to shapefiles.
4. Creating Features
The GeoTools documentation specifies that a feature is anything that can be drawn on a map, like a city or some landmark. And, as we mentioned, once created, features can then be saved into files called shapefiles.
4.1. Keeping Geospatial Data
Before creating a feature, we need to know its geospatial data or the longitude and latitude coordinates of its location on earth. As for attribute data, we need to know the name of the feature we want to create.
This information can be found on the web. Some sites like simplemaps.com or maxmind.com offer free databases with geospatial data.
When we know the longitude and latitude of a city, we can easily store them in some object. We can use a Map object that will hold the city name and a list of its coordinates.
Let’s create a helper method to ease the storing of data inside our Map object:
private static void addToLocationMap( String name, double lat, double lng, Map<String, List<Double>> locations) { List<Double> coordinates = new ArrayList<>(); coordinates.add(lat); coordinates.add(lng); locations.put(name, coordinates); }
Now let’s fill in our Map object:
Map<String, List<Double>> locations = new HashMap<>(); addToLocationMap("Bangkok", 13.752222, 100.493889, locations); addToLocationMap("New York", 53.083333, -0.15, locations); addToLocationMap("Cape Town", -33.925278, 18.423889, locations); addToLocationMap("Sydney", -33.859972, 151.211111, locations); addToLocationMap("Ottawa", 45.420833, -75.69, locations); addToLocationMap("Cairo", 30.07708, 31.285909, locations);
If we download some CSV database that contains this data, we can easily create a reader to retrieve the data instead of keeping it in an object like here.
4.2. Defining Feature Types
So, now we have a map of cities. To be able to create features with this data, we’ll need to define their type first. GeoTools offers two ways of defining feature types.
One way is to use the createType method of the DataUtilites class:
SimpleFeatureType TYPE = DataUtilities.createType( "Location", "location:Point:srid=4326," + "name:String");
Another way is to use a SimpleFeatureTypeBuilder, which provides more flexibility. For example, we can set the Coordinate Reference System for the type, and we can set a maximum length for the name field:
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.setName("Location"); builder.setCRS(DefaultGeographicCRS.WGS84); builder .add("Location", Point.class); .length(15) .add("Name", String.class); SimpleFeatureType CITY = builder.buildFeatureType();
Both types store the same information. The location of the city is stored as a Point, and the name of the city is stored as a String.
You probably noticed that the type variables TYPE and CITY are named with all capital letters, like constants. Type variables should be treated as final variables and should not be changed after they are created, so this way of naming can be used to indicate just that.
4.3. Feature Creation and Feature Collections
Once we have the feature type defined and we have an object that has the data needed to create features, we can start creating them with their builder.
Let’s instantiate a SimpleFeatureBuilder providing our feature type:
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(CITY);
We’ll also need a collection to store all the created feature objects:
DefaultFeatureCollection collection = new DefaultFeatureCollection();
Since we declared in our feature type to hold a Point for the location, we’ll need to create points for our cities based on their coordinates. We can do this with the GeoTools’s JTSGeometryFactoryFinder:
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(null);
Note that we can also use other Geometry classes like Line and Polygon.
We can create a function that will help us put features in the collection:
private static Function<Map.Entry<String, List<Double>>, SimpleFeature> toFeature(SimpleFeatureType CITY, GeometryFactory geometryFactory) { return location -> { Point point = geometryFactory.createPoint( new Coordinate(location.getValue() .get(0), location.getValue().get(1))); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(CITY); featureBuilder.add(point); featureBuilder.add(location.getKey()); return featureBuilder.buildFeature(null); }; }
Once we have the builder and the collection, by using the previously created function, we can create features and store them in our collection:
locations.entrySet().stream() .map(toFeature(CITY, geometryFactory)) .forEach(collection::add);
The collection now contains all the features created based on our Map object that held the geospatial data.
5. Creating a DataStore
GeoTools contains a DataStore API that is used to represent a source of geospatial data. This source can be a file, a database, or some service that returns data. We can use a DataStoreFactory to create our DataStore, which will contain our features.
Let’s set the file that will contain the features:
File shapeFile = new File( new File(".").getAbsolutePath() + "shapefile.shp");
Now, let’s set the parameters that we are going to use to tell the DataStoreFactory which file to use and indicate that we need to store a spatial index when we create our DataStore:
Map<String, Serializable> params = new HashMap<>(); params.put("url", shapeFile.toURI().toURL()); params.put("create spatial index", Boolean.TRUE);
Let’s create the DataStoreFactory using the parameters we just created, and use that factory to create the DataStore:
ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory(); ShapefileDataStore dataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params); dataStore.createSchema(CITY);
6. Writing to a Shapefile
The last step that we need to do is to write our data to a shapefile. To do this safely, we are going to use the Transaction interface that is a part of the GeoTools API.
This interface gives us the possibility to easily commit our the changes to the file. It also provides a way to perform a rollback of the unsuccessful changes if some problem occurs while writing to the file:
Transaction transaction = new DefaultTransaction("create"); String typeName = dataStore.getTypeNames()[0]; SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName); if (featureSource instanceof SimpleFeatureStore) { SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; featureStore.setTransaction(transaction); try { featureStore.addFeatures(collection); transaction.commit(); } catch (Exception problem) { transaction.rollback(); } finally { transaction.close(); } }
The SimpleFeatureSource is used to read features, and the SimpleFeatureStore is used for read/write access. It is specified in the GeoTools documentation that using the instanceof method for checking if we can write to the file is the right way to do so.
This shapefile can later be opened with any GIS viewer that has shapefile support.
7. Conclusion
In this article, we saw how we can make use of the GeoTools library to do some very interesting geo-spatial work.
Although the example was simple, it can be extended and used for creating rich shapefiles for various purposes.
We should have in mind that GeoTools is a vibrant library, and this article just serves as a basic introduction to the library. Also, GeoTools is not limited to creating vector data types only – it can also be used to create or to work with raster data types.
You can find the full example code used in this article in our GitHub project. This is a Maven project, so you should be able to import it and run it as it is.