GeoTools is an open source Java library for authoring GIS software and applications.
It is a fully equipped library adhering to the Open Geospatial Consortium (OGC) standards and providing graphical user interface (GUI).
https://docs.geotools.org/latest/userguide/welcome/standards.html
This lecture demonstrates techniques for using GeoTools in a Grails app through the sequence of three projects:
- gradle-geotools
- grails-bean
- grails-geotools
The final project, grails-geotools, focuses on using shapefiles for querying geographic information and rendering the information in a view.
Before studying these projects, you should have some acquaintance with GeoTools and GIS terminology. I recommending doing a few lectures at GeoTools, at a minimum:
- Maven Quickstart – https://docs.geotools.org/latest/userguide/tutorial/quickstart/maven.html
- Feature Tutorial – https://docs.geotools.org/latest/userguide/tutorial/feature/csv2shp.html
- Query Tutorial – https://docs.geotools.org/latest/userguide/tutorial/filter/query.html
GeoTools developers prefer to use Maven for their build tool.
Maven is one of the original Java build tools and is a substitute for Gradle. It is worthwhile becoming acquainted with it. Fortunately you can download it using SDKMAN.
Look for it using
sdk list mvn
In the Maven Quickstart, you will learn how to display a shapefile in a SWING map. Shapefiles is ESRI standard for a geodatabase. The shapefile and the associated “side car” files are ESRI proprietary formats for representing geography and associated attributes. You can think of it as a geodatabase.
A special note: The current version of GeoTools requires Java 11.
In the Feature Tutorial, you will gain more understanding of shapefiles by making shapefiles from a CSV file. Each item in the shape is called a “feature”. The feature has a “geometry” and associated properties.
In the Query Tutorial, you learn how to make queries into the Shapefile. Rather you make queries into the datastore that you build from the shapefiles. The queries can be geographical such as does a feature contain a point. Although relational databases use SQL, the Query Tutorial uses the Common Query Language, CQL.
The Challenge
After completing the three tutorials, you should realize that there are still some challenges for using GeoTools in a Grails app.
- GeoTools uses Maven for the build while Grails uses Gradle.
- How to organize GeoTools code so the Grails controllers have easy access to its functions.
The first project in this lecture demonstrates how to build a GeoTools app using Gradle. It is not a difficult challenge, but requires knowledge of both Maven and Gradle. The app is simple. It reads a shapefile, builds a datastore and then query it.
The best way for Grails controllers to access datastores is from a service. But we don’t want each query into the datastore to reread and load the shapefile. So our program should create a Java object representing the datastore. But this leads to another problem, how should the Service access the datastore object? The technology to use is called “Spring Beans”. The second project in this lecture will briefly explain the Spring Bean technology and how to use them in a Grails app.
The final project in this lecture combines the two previous projects to render geographical information in a view. A bean will represent the datastore built from the shapefiles and a service will make queries into the feature sources.
You can clone the code at:
https://github.com/2024-UI-SP/GeoTools-Lecture
gradle-geotools
The gradle-geotools project demonstrates developing a simple GeoTools app in a Gradle project. It was initialized in the gradle-geotools directory with Gradle 7.2 using Java 11. We make the basic Gradle project by entering
gradle init
The script will ask questions about the project:
- Select type of build to generate: selected 2. application
- Select implementation language: selected 3. java (default)
- Split functionality across multiple subprojects?: selected 1 no (default)
- Select build script DSL: selected 1 Groovy (default)
- Select test framework: 1. JUint 4
- Project name (default: gradle-geotools):
- Source package (default: gradle.geotools):
The script makes a “hello world” app. The code for the class is located in app/src/main/gradle/geotools/App.java
. You can run the “hello world” app by entering
./gradlew run
To make the project we only need to
- Edit
app/src/main/java/gradle/geotools/App.java
- Put the shapefiles in
app/src/main/resources/gradle/geotools/
- Edit
app/build.gradle
First, we edit app/src/main/java/gradle/geotools/App.java
. We replace the code with:
/* * This Java source file was generated by the Gradle 'init' task. */ package gradle.geotools; // The following imports are for working with files import java.io.File; import java.net.URL; // The following imports are for making the data store and source import org.geotools.api.data.FileDataStore; import org.geotools.api.data.FileDataStoreFinder; import org.geotools.api.data.SimpleFeatureSource; // The following imports are for making the query import org.geotools.filter.text.cql2.CQL; // this needs gt-swing dependency import org.geotools.api.filter.Filter; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.api.feature.type.FeatureType; import org.geotools.api.data.Query; public class App { public static void main(String[] args) throws Exception { System.out.println("Running App.main() \n"); /* * Make the data store and feature source */ // Get the shapefile using getResource() URL url = App.class.getResource("countries.shp"); // Just to make sure we got the right file System.out.println("URL: " + url+ "\n"); File file = new File(url.toURI()); // Use the file to make the data store using FileDataStoreFinder FileDataStore store = FileDataStoreFinder.getDataStore(file); // Get the feature source from the store using getFeatureSource() SimpleFeatureSource source = store.getFeatureSource(); // Do some printing String typeName = store.getTypeNames()[0]; // There is only one type System.out.println("typeName is the name of datastore. It is almost always the name of the shapefile."); System.out.println("typeName: " + typeName + "\n"); FeatureType schema = source.getSchema(); System.out.println("schema is the list of shapefile's attributes and types"); System.out.println("schema: " + schema + "\n"); /* * Make the query and filter to show all countries and population */ System.out.println("Result from querying for all countries and population"); String cqlString = "include"; // Include all features Filter filter = CQL.toFilter(cqlString); Query query = new Query(typeName, filter, new String[] { "CNTRY_NAME", "POP_CNTRY" }); System.out.println("query: " + query + "\n"); // Get the features, i.e. the "result set" SimpleFeatureCollection features = source.getFeatures(query); try (SimpleFeatureIterator iterator = features.features()) { while (iterator.hasNext()) { SimpleFeature feature = iterator.next(); // process feature for ( Object value : feature.getAttributes() ) { System.out.print(value + " "); } System.out.println(); } } System.out.println(" \n "); /* * Make the query and filter to find the country that contains the a point. */ System.out.println("Result from querying for the country that contains a point"); filter = CQL.toFilter("contains(the_geom, POINT(-100 40))"); // Note logitude first query = new Query(typeName, filter, new String[] { "CNTRY_NAME", "POP_CNTRY" }); System.out.println("query: " + query + "\n"); // process the features. All this for a single item because features is a collection features = source.getFeatures(query); try (SimpleFeatureIterator iterator = features.features()) { while (iterator.hasNext()) { SimpleFeature feature = iterator.next(); for ( Object value : feature.getAttributes() ) { System.out.print(value + " "); } System.out.println(); } } System.out.println(" \n "); } }
The code is the simplest code that I could write to load a shapefile to create the datastore, make a few queries into the feature source, and process the feature collection resulting from the queries
Note the Java File is made using class.getResources
URL url = App.class.getResource("countries.shp"); // Just to make sure we got the right file System.out.println("URL: " + url+ "\n"); File file = new File(url.toURI());
https://docs.oracle.com/javase/6/docs/api/java/lang/Class.html#getResource(java.lang.String)
Now we add the shapefiles. Conveniently, the initializing script made a resources directory in app/src/main/. But we need the path to match the class package, so we make app/src/main/resources/gradle/geotools/shapefiles
, and put the shapefiles in the shapefiles directory. The countries shapefiles are from data-v1_2.zip
. You can find the zip file at:
http://udig.refractions.net/files/docs/
Note you should use the code from the Query Tutorial to examine the shapefiles and learn the property names.
The code uses a CQL string to make the filter. Because we want only a few properties of the features, we need to construct a Query
.
System.out.println("Result from querying for all countries and population"); String cqlString = "include"; // Include all features Filter filter = CQL.toFilter(cqlString); Query query = new Query(typeName, filter, new String[] { "CNTRY_NAME", "POP_CNTRY" }); System.out.println("query: " + query + "\n");
To examine the feature collection from the result of the query, the code MUST use try
and the iterator:
try (SimpleFeatureIterator iterator = features.features()) { while (iterator.hasNext()) { ... } }
To summarize, the code needs to:
- Make the SimpleFileDataStore using a FileDataStoreFinder with a File
- Get the SimpleDataFeatureSource
- Make the Query with a Filter using a CQL string
- Get the “results set” as a SimpleFeatureCollection
- try to iterate through the SimpleFeatures using SimpleFeatureIterator
Finally to build the project, we need to edit build.gradle
. The GeoTools jars are not located in Maven Central, rather they are located at OSGeo repositories, so we need to add them to the repositories section of build.gradle.
maven { url "https://repo.osgeo.org/repository/snapshot/" mavenContent { snapshotsOnly() } } maven { url "https://repo.osgeo.org/repository/release/" mavenContent { releasesOnly() } }
I determined these unusual declarations from
https://docs.gradle.org/current/userguide/declaring_repositories.html#maven_repository_filtering
and inspecting the mvn declarations in the GeoTools tutorials. Also important is that these declarations must be placed before the maven central declaration because the javax.mediajai_core POM is broken in maven central. Gradle searches the repositories in order so if maven central is searched first. It discovers the POM for the package, but the package does not exist. This brakes Maven and Gradle.
Now add the project dependencies in the dependencies section
// Provides support for reading and writing shapefiles // Needed to make the data store implementation "org.geotools:gt-shapefile:$geotoolsVersion" // Needed for CQL filters implementation "org.geotools:gt-swing:$geotoolsVersion"
Most of the classes used by App.java
are found in the gt-shapefile package. The gt-swing package is only required for org.geotools.filter.text.cql2.CQL
. Otherwise gt-swing contains classes for building GUIs.
We are done and can run the project with
./gradlew run
Look for the print output in the console. Study the code. Try writing a different query and processing the resulting feature collection.
grails-beans
This project demonstrates how Spring Beans are used in a Grails project. The bean only prints out to the terminal. We want to learn:
- where the beans are defined
- how they are integrated in the Grails project
- how they can be used in the project, i.e. injected
The Grails documentation for Spring Beans does not explain how to define and use beans.
https://docs.grails.org/6.2.0/guide/single.html#spring
We have to study the Spring documentation to learn more about beans:
https://docs.spring.io/spring-framework/reference/core/beans/introduction.html
You will want to read more:
https://docs.spring.io/spring-framework/reference/core/beans/basics.html
https://docs.spring.io/spring-framework/reference/core/beans/definition.html
https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html
If the Spring documentation is too technical, you may find this stackoverflow post more gentle.
https://stackoverflow.com/questions/17193365/what-in-the-world-are-spring-beans
The basic goal for the bean framework is that the developer should not have to worry about managing when and how to instantiate simple Java objects (called beans) and other classes in the application have access to the beans. Rather the “container” manages instantiating beans and making easy access to the instantiated bean for the other classes in the application. For this to happen properly, naturally, the developer needs to configure and register the bean with the “container”. Configuration includes:
- The name of the bean (called the id in Spring)
- The class defining the bean
- The scope of the bean, e.g. singleton, session, request etc. Note singleton is default scope
- other optional properties of the bean
Traditionally Spring used XML in a resources/spring.xml file to configure the bean. Grails uses Groovy to configure beans in the conf/spring/resources.groovy
You should realize that in a Grails project almost all groovy classes are beans, this includes:
- Domains
- Controllers
- Services
- BootStrap.groovy
Grails manages configuration of these beans.
Now we can look at the code. The basic Grail app was made with Grails 5.3 and Java 11 by entering
grails create-app grails-bean
This creates a simple web app using the grails.bean package name.
First we will study a simple bean. The class files that you make yourself are located in src/main/groovy followed by the package directories. In this case
src/main/groovy/grails/bean/MyBean.groovy
where “grails/bean/” is the package directory.
package grails.bean // Don't forget! /* * A simple bean to demonstrate instantiation, injection and use in Grails. * See conf/spring/resources.groovy for instantiation. * See BootStrap.groovy for injection and use. */ class MyBean { String name private String constructorName MyBean(String constructorName) { println "In MyBean constructor" this.constructorName = constructorName } void printName() { println "MyBean name: ${name}" } void printConstructorName() { println "MyBean constructorName: ${constructorName}" } }
This bean has only two attributes, name
and constructorName
, and two methods to print the attributes. Note that constructorName
attribute is set by the constructor, while the name
attribute is public, so can be set by anyone.
After writing the bean definition, you can run the application, but nothing will happen, although it will be compiled.
To instantiate bean, we need to configure and register it in grails-app/conf/spring/resource.groovy
import grails.bean.MyBean /* * beans registers and instantiates beans. * See https://docs.grails.org/6.2.0/guide/spring.html for simple examples. * See https://stackoverflow.com/questions/22400078/inject-constructor-argument-spring-resource-file-with-grails-groovy * for how to inject constructor arguments. */ beans = { // This closure uses Spring Domain Specific Language (DSL). // The first argument is the class name and following argument are the constructor arguments myBean(MyBean, "My_Bean_Constructor") { // Here public variables are set name = "My_Bean" } }
What looks like a “method call” names the bean, myBean
. This is what other classes use to reference the bean. The minimal declaration for a bean is
package <package> <Bean class> beans = { <bean id>(<Bean class>) }
This will name a singleton bean and instantiate it using the default constructor (i.e. with on arguments). In the method body, you can define public attributes. If the constructor has arguments then they are listed after the class in the bean declaration.
You can run the code now, and notice that Grails instantiate the bean because the constructor prints.
Now to use the bean by calling its method. We could use the bean in almost any other part of the application. BootStrap.groovy runs before any other Grails artifacts, so we use it there.
package grails.bean class BootStrap { def myBean // This is injecting myBean def init = { servletContext -> println "In BootStrap init" /* * Demonstrates how to use a bean in Grails by calling methods. */ myBean.printConstructorName() myBean.printName() println "\n" } def destroy = { } }
To inject the bean, all we need to do is declare the bean’s name as a class attribute
def myBean
That is it. Running the Grails app now will make all the print outs.
./gradlew bootRun
Very cool! Try making your own bean.
grails-geotools
We have learned all the technologies for integrating GeoTools into a Grails app. First we should think about how to architect the code. Best practice is that controllers use services to access datastores. But we do not want the service to be creating the datastore for every call, so we should use a singleton bean to represent the datastore. The constructor for the bean can create the datastore. So the architecture is
MyFeatureSouce.groovy
– a bean that creates the datastore, specifically aSimpleFeatureSource
, and offer access to the datastoreFeatureService.groovy
– a service with reference tomyFeatureSource
and has a method to make theQuery
and process theSimpleFeatures
from theSimpleFeatureCollection
.
The FeatureService will return the country name for a latitude, longitude. Finally we want to render the service in a controller, CountryController.groovy
.
Now we can look at the project code. The project was made using Grails 5.3 and Java 11, and entering
./grails create-app grails-geotools
which makes a web application with the package name “grails.geotools”.
First we edit the build.gradle, the repositories and dependencies. This is identical to the gradle-geotools, so I’ll not display it again.
Now make our bean, MyFeatureSource.groovy
, in src/main/groovy/grails/geotools/
.
package grails.geotools // For working with files import java.io.File import java.net.URL // For making the data store and source import org.geotools.api.data.FileDataStore import org.geotools.api.data.FileDataStoreFinder import org.geotools.api.data.SimpleFeatureSource /* * A bean generating a SimpleFeatureSource from a shapefile. * See conf/spring/resources.groovy for registrating. * See BootStrap.groovy and CountryController for use. */ class MyFeatureSource { private String shapefileName private SimpleFeatureSource source MyFeatureSource(String shapefileName) { this.shapefileName = shapefileName println "\n" println "In MyFeatureSource constructor" // Make File URL url = MyFeatureSource.class.getResource("shapefiles/${shapefileName}") File file = new File(url.toURI()) println("File: " + file) // Make DataStore FileDataStore store = FileDataStoreFinder.getDataStore(file) println("Type Names: " + store.getTypeNames()) source = store.getFeatureSource() } // Just for development void printHi() { println "\n" println "Hi from MyFeatureSource!" println "MyFeatureSource shapefileName: ${shapefileName}" } SimpleFeatureSource getSource() { return source } }
Given a shapefile name the constructor makes the SimpleFeatureSource, source
. There is only one method, getSource
which the service will use. The code should look familiar.
We put the shapefiles in src/main/resources/grails/geotools/shapefiles/
Now, we register the bean in spring/resources.groovy
import grails.geotools.MyFeatureSource /* * beans resigisters and instantiates beans. * See https://docs.grails.org/6.2.0/guide/spring.html for simple examples. * See https://stackoverflow.com/questions/22400078/inject-constructor-argument-spring-resource-file-with-grails-groovy * for how to inject constructor arguments. */ beans = { // The first argument is the constructor, the second is the argument. myFeatureSource(MyFeatureSource, "countries.shp") }
Next, we make the service, FeatureService.groovy
, by entering
./grailsw create-service grails.geotools.FeatureService
https://docs.grails.org/6.2.0/ref/Command%20Line/create-service.html
We edit FeatureService.groovy
package grails.geotools // For Query import org.geotools.filter.text.cql2.CQL import org.geotools.api.data.Query import org.geotools.api.filter.Filter // For Features import org.geotools.data.simple.SimpleFeatureCollection import org.geotools.data.simple.SimpleFeatureIterator import org.geotools.api.feature.simple.SimpleFeature import grails.gorm.transactions.Transactional @Transactional class FeatureService { // FeatureService needs a FeatureSource def myFeatureSource // from src/main/groovy/geotools/MyFeatureSource.groovy def getCountryName(double latitude, double longitude) { println "\n" println "In FeatureService getCountryName" // Get Source def source = myFeatureSource.getSource() // Make Query String filterString = "contains(the_geom, POINT(${longitude} ${latitude}))" Filter filter = CQL.toFilter(filterString) String typeName = source.getSchema().getTypeName() String [] propertyNames = new String[] { "CNTRY_NAME" } Query query = new Query(typeName, filter, propertyNames) // Get Features and Collect Country Names SimpleFeatureCollection features = source.getFeatures(query) def countryNames = [] try (SimpleFeatureIterator iterator = features.features()) { while (iterator.hasNext()) { SimpleFeature feature = iterator.next() println "Feature: ${feature}" println "Feature Attribute CNTRY_NAME: ${feature.getAttribute("CNTRY_NAME")}" countryNames << feature.getAttribute("CNTRY_NAME") } } return countryNames } }
FeatureService has an attribute referencing myFeautureSource, so the bean will be injected. There is only one method, getCountryName(double latitude, double longitude)
. The code in the method body should be familiar.
I used Bootstrap.groovy during development to assure that the bean and services were made properly.
We make the controller by entering
./grailsw create-controller Country
Finally, we edit CountryController.groovy
.
package grails.geotools /* * A controller demonstrating FeatureService. * See grails-app/services/geotools/FeatureService.groovy for the service. */ class CountryController { // Inject FeatureService def featureService def index() { // Use FeatureService double latitude = 38.0 double longitude = -77.0 def countryNames = featureService.getCountryName(latitude, longitude) // render the view render "In CountryController: Country Names: ${countryNames}" } }
Very simple, we don’t even need to make the index view.
You can run the app
./gradle bootRun
and open the browser to localhost:8080/country
That is it. The hard part was figuring out how to use Gradle to make a GeoTools app and how to use Spring Beans in Grails.
To summarize to use GeoTools in a Grails app, you should
- Edit build.gradle, adding the repositories and dependencies.
- Make a bean for the datastore
- Register the bean
- Make a service to query using the bean
- Use the service in a controller to render the results in a view.