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
});