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.
the relevant points to note are:
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:
- drop the jts jar in the lib directory
- create a controller and define the relevant methods
- 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"] }
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 });