GIS Programming Using GeoTools

GeoTools is an open source Java library for authoring GIS software and applications. 

https://geotools.org

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:

  1. gradle-geotools
  2. grails-bean
  3. 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:

  1. Maven Quickstart – https://docs.geotools.org/latest/userguide/tutorial/quickstart/maven.html
  2. Feature Tutorial – https://docs.geotools.org/latest/userguide/tutorial/feature/csv2shp.html 
  3. Query Tutorial – https://docs.geotools.org/latest/userguide/tutorial/filter/query.html

GeoTools developers prefer to use Maven for their build tool.

https://maven.apache.org

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.

https://sdkman.io

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 

  1. Edit app/src/main/java/gradle/geotools/App.java
  2. Put the shapefiles in app/src/main/resources/gradle/geotools/
  3. 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)

https://stackoverflow.com/questions/6608795/what-is-the-difference-between-class-getresource-and-classloader-getresource

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: 

  1. Make the SimpleFileDataStore using a FileDataStoreFinder with a File
  2. Get the SimpleDataFeatureSource 
  3. Make the Query with a Filter using a CQL string
  4. Get the “results set” as a SimpleFeatureCollection
  5. 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.  

https://stackoverflow.com/questions/26993105/i-get-an-error-downloading-javax-media-jai-core1-1-3-from-maven-central

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. 

https://stackoverflow.com/questions/22400078/inject-constructor-argument-spring-resource-file-with-grails-groovy

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 a SimpleFeatureSource, and offer access to the datastore
  • FeatureService.groovy – a service with reference to myFeatureSource and has a method to make the Query and process the SimpleFeatures from  the SimpleFeatureCollection.

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

  1. Edit build.gradle, adding the repositories and dependencies.
  2. Make a bean for the datastore
  3. Register the bean
  4. Make a service to query using the bean
  5. Use the service in a controller to render the results in a view.