Thursday, January 05, 2012

JTS processing Grails servlet

Sometimes it is convenient to make certain topological operations available in a web-based gis. For a gis that makes heavy use of Javascript (like OpenLayers-based ones) it might be worth looking at jSTS, a Javascript port of JTS.

For all the rest and for those who don't want to load yet another library in the browser you can always write a Grails controller that encapsulates common JTS operations like buffer, intersection, union, etc.

Assuming you are familiar with Grails the steps are as follows:
  1. drop the jts jar in the lib directory
  2. create a controller and define the relevant methods
  3. define a url mapping to prettify the calls
Step 1 is trivial, so we'll go straight to 2. Create a controller and call it JtsController, then open the source file and paste this code:

import com.vividsolutions.jts.geom.*
import com.vividsolutions.jts.io.*
import com.vividsolutions.jts.operation.overlay.snap.*
import grails.converters.JSON

import grails.plugins.springsecurity.Secured


@Secured(['IS_AUTHENTICATED_FULLY'])
class JtsController {
 def exec = {
  def pm = new PrecisionModel(PrecisionModel.FLOATING_SINGLE);
  def fact = new GeometryFactory(pm);
  def wktRdr = new WKTReader(fact); 
  
  def text = request.reader.text
  
  if(params.operation) {   
   def geometries = text.split("\\*")
                 Geometry A = selfSnap(wktRdr.read(geometries[0]))
   Geometry B = null
                  Geometry C = null
   if (geometries.length==2)
    B = selfSnap(wktRdr.read(geometries[1]))
   
   if ("area".equalsIgnoreCase(params.operation))
    C = A;
   else if ("intersection".equalsIgnoreCase(params.operation))
    C = A.intersection(B);
   else if ("union".equalsIgnoreCase(params.operation))
    C = A.union(B);
   else if ("buffer".equalsIgnoreCase(params.operation)) {
    // defaults to 25
    C = A.buffer(25);
   } else if (params.operation.startsWith("buffer")) {
    // parametric buffer
    def distance=(String)params.operation.substring(6)
    C = A.buffer(Double.parseDouble(distance));
   } else {
    render text: "${params.operation} not supported.", status: 400
    return false
   }
   
   render(contentType: "text/json") {
    geom(C.toText())
    area(C.getArea())
   }
  } else {
   render text: "Please supply an operation to be performed.", status: 400
   return false
  }
 }

 def selfSnap(Geometry g)
 {
  double snapTol = GeometrySnapper.computeOverlaySnapTolerance(g);
  GeometrySnapper snapper = new GeometrySnapper(g);
  Geometry snapped = snapper.snapTo(g, snapTol);
  // need to "clean" snapped geometry - use buffer(0) as a simple way to do this
  Geometry fix = snapped.buffer(0);
  return fix;
 }
}

the relevant points to note are:
  • the geometries (up to two) are sent in the POST body in WKT format, separated by a * (you may change that, I just happened to like the *)
  • both geometries are 'cleaned' with a self-snap operation to prevent invalid geometries from blocking the operation (in my case I had many, cleaning was not an option as I am not the owner of the dataset)
  • the operation is specified as part of the url, thanks to a custom url mapping
The url mapping (step 3) is as follows:

"/jts/$operation"(controller: "jts") {
   action = [GET: "exec", POST: "exec"]
}

A JTS buffer operation can then be invoked in Sproutcore as follows:

SC.Request.postUrl("/app/jts/buffer")
     .notify(this, 'didPerformGeoOperation')
     .send(geom1.toString() + "*");
A JTS intersection operation in jQuery :
$.ajax({
  type: 'POST',
  url: "/app/jts/intersection",
  data: geom1.toString() + "*" + geom2.toString(),
  success: didPerformGeoOperation
});

No comments: