Tuesday, January 12, 2010

Extend GeoServer with customized OWS service :: part 5

In previous post of this series, I summarize the basic work flow of GeoServer OWS dispatcher dispatching OWS requests based on request parameters “service”, “version”, and “request”. We already know that in “ags-ows” service project, the “export” method will finally be called to process the request, and it takes a request bean (AgsOwsExportRequest) instance as input parameter. So in this post let’s pick up from that point and have a look at how response is produced.

We already know ows dispatcher dispatches requests, but after it finds and calls the appropriate method in appropriate service class with request bean it doesn’t stop it job. Instead it keeps doing its job to produce the response.
1: ...
2: // this is where OWS dispatcher finds the service and method and execute it
3: // execute it
4: Object result = execute(request, operation);
5: 
6: // this is where OWS dispatcher keeps its job to produce response
7: //write the response
8: if (result != null) {
9:     response(result, request, operation);
10: }
11: ...
The code snippet above is from OWS dispatcher, the “execute” method actually calls the service operation method (in case of “ags-ows” service, it is the export method in AarcGISServerOWSService class) and return whatever it returns, which will be assigned to “result”. Look at export method again
1: // export method in ArcGISServerOWSService class
2: ...
3: public AgsOwsExportResponse export(AgsOwsExportRequest request) {
4:     return new AgsOwsExportResponse(this.geoServer);
5: }
6: ...
the result is actually in type of AgsOwsExportResponse (you saw that in part 2 but I told you not to worry about then). AgsOwsExportResponse is a subclass of org.geoserver.ows.Response, which is the core element for an OWS service to produce responses. Before we jump into the implementation of AgsOwsExportResponse, let’s first look at how ows dispatcher uses response class. Go back to dispatcher code again and step into the “response(result, request, operation);” method we saw before:
1: ...
2: // response method in org.geoserver.ows.Dispatcher class
3: void response(Object result, Request req, Operation opDescriptor)
4:     throws Throwable {
5: 
6:     // loop through all registered subclass of org.geoserver.ows.Response 
7:     //   in application context and try to match a unique response type based on
8:     //   the class type of result and the binding class of Response subclass   
9: 
10:     ...
11:     ... 
12:     Response response = (Response) responses.get(0);
13:     ...
14:     // you need to implement getMimeType() in Response subclass 
15:     req.httpResponse.setContentType(response.getMimeType(result, opDescriptor));
16:     ...
17:     // you need to implement write() in Response subclass
18:     response.write(result, output, opDescriptor);
19:     ...
20:     // finally flush out the response to clients
21:     req.httpResponse.getOutputStream().flush();
22: }
23: ...
Dispatcher will loop through all the subclasses of org.geoserver.ows.Response registered in application context and find a unique response class whose binding class matches the class type of result object returned by service operation method. Take AgsOwsExportResponse as an example, since it’s binding class is itself which exactly matches the result object returned by export method of ArcGISServerOWSService class, it will be picked up by dispatcher.

After dispatcher locates the appropriate response class, two methods actually matters and thus must be implemented. (1) getMimeType(), which returns mime type string of your response, and it will be set directly in the header of http response back to clients; (2) write(), which writes response body in output stream of http response back to clients. Below is a sample implementation of AgsOwsExportResponse class and how it is registered in application context:
1: public class AgsOwsExportResponse extends Response {
2: 
3:     public AgsOwsExportResponse() {
4:         super(AgsOwsExportResponse.class);
5:     }
6: 
7:     @Override
8:     public String getMimeType(Object value, Operation operation)
9:         throws ServiceException {  
10:         // to simplify the problem, always return image/png
11:         // but in your own ows service, you can decided based on what you support 
12:         //    and what clients request
13:         return "image/png";
14:     }
15:     ...
16:     @Override
17:     public void write(Object value, OutputStream output, Operation operation)
18:         throws IOException, ServiceException {  
19:         ...
20:         // write response map image back to clients 
21:         ...
22:     }
23: }

1: <!-- service operation response -->
2: <bean id="agsOwsCapabilitiesResponse" 
3:     class="org.geoserver.ows.arcgisserver.responses.AgsOwsCapabilitiesResponse"
4:     singleton="false">     
5:     <constructor-arg ref="geoServer"/>   
6: </bean>
Notice the binding class is AgsOwsExportResponse itself but it could definitely be something else if the service operation method export chooses to return something else (org.geoserver.wcs.responses.GetCapabilitiesResponse is a good example of that). As I mentioned in the beginning, “image/png” is hard coded in getMimeType() to simplify the problem. And finally in write() I need to produce a map image and write it in output stream. The implementation of write() method deserves another separate paragraph of explanation which I will do right after, and so far ows dispatcher has finished the whole life cycle for an ows service request dispatching.

Implement Write() in AgsOwsExportResponse and AgsOwsMapProducer

Implementing Write() in AgsOwsExportResponse can be as simple as less than 10 lines of code if you only want to produce and output some simple static content back to clients.
1: private void write(Object value, OutputStream output, Operation operation) 
2:     throws IOException, ServiceException {
3:     String responseStr = "Static content";
4:     OutputStreamWriter writer = new OutputStreamWriter(output);
5:     writer.write(responseStr);
6:     writer.flush();
7: }
But if it is creating a map image, it can definitely become way more complicated (org.vfny.geoserver.wms.responses.GetMapResponse in wms project is a good example), which sometimes needs one or more helper classes (those map producer classes under package org.vfny.geoserver.wms.responses.map are very good example). In AgsOwsExportResponse of “ags-ows” service project I sort of borrowed the idea from GeoServer WMS GetMapResponse class and PNGMapProducer class, in which a helper class similar to PNGMapProducer called AgsOwsMapProducer is created to render map. Of course I omitted most of the details and keeps just enough code to create an map image in png format and write out to client.

In the write() method of AgsOwsExportResponse, I create an instance of GraphicEnhancedMapContext class (from GeoTools library) and pass it to AgsOwsMapProducer. The GraphicEnhancedMapContext instance basically stores all information (e.g. map projection, background color, transparent, layers, styles etc.) needed to produce a map image using GeoTools.
1: private void write(Object value, OutputStream output, Operation operation)
2:       throws IOException, ServiceException {    
3:     
4:     AgsOwsExportResponse exportResponse = (AgsOwsExportResponse)value;
5:     AgsOwsExportRequest exportRequest 
6:         = (AgsOwsExportRequest)OwsUtils.parameter(operation.getParameters(), AgsOwsExportRequest.class);
7:     
8:     /*
9:      * the main purpose here is to create mapContext {GraphicEnhancedMapContext} -
10:      * - and pass it to map producer to generate the map
11:      * - borrow the idea from GetMapResponse but omitted a lot details
12:      */    
13:     // requested image crs
14:     final CoordinateReferenceSystem imageSR = exportRequest.getImageSR();
15:     // requested layers
16:     final LayerInfo[] layers = exportRequest.getLayers();          
17:     // initialize mapContext
18:     this.mapContext = new GraphicEnhancedMapContext();    
19:     try {
20:       this.mapContext.setCoordinateReferenceSystem(imageSR);
21:     } catch(FactoryException e) {
22:       e.printStackTrace();
23:     } catch(TransformException e) {
24:       e.printStackTrace();
25:     }        
26:     // bbox and bbox crs
27:     final CoordinateReferenceSystem bboxSR = exportRequest.getBboxSR();
28:     final Envelope bbox = exportRequest.getBbox();
29:     if(bboxSR != null) {
30:       this.mapContext.setAreaOfInterest(bbox, bboxSR);
31:     } else {
32:       // TODO: should throw exception, no?
33:       this.mapContext.setAreaOfInterest(bbox, DefaultGeographicCRS.WGS84);
34:     }
35:     
36:     this.mapContext.setMapWidth(exportRequest.getWidth());
37:     this.mapContext.setMapHeight(exportRequest.getHeight());
38:     this.mapContext.setBgColor(exportRequest.getBgColor());
39:     this.mapContext.setTransparent(exportRequest.isTransparent());
40:     
41:     try {
42:       for (int i=0; i<layers.length; i++) {      
43:         final Style layerStyle = layers[i].getDefaultStyle().getStyle();        
44:         final MapLayer layer;                
45:         if(layers[i].getType().getCode() == LayerInfo.Type.VECTOR.getCode()) {
46:           FeatureSource<? extends FeatureType, ? extends Feature> featureSource;
47:           FeatureTypeInfo resource = (FeatureTypeInfo)layers[i].getResource();
48:           if(resource.getStore() == null || resource.getStore().getDataStore(null) == null) {
49:                   throw new IOException("");
50:               }
51:           Hints hints = new Hints(ResourcePool.REPROJECT, Boolean.valueOf(false));
52:           featureSource = resource.getFeatureSource(null, hints);
53:           
54:           layer = new FeatureSourceMapLayer(featureSource, layerStyle);
55:                     layer.setTitle(layers[i].getResource().getName());                    
56:                     // use default filter and version in DefaultQuery
57:                     final DefaultQuery definitionQuery = new DefaultQuery(featureSource.getSchema().getName().getLocalPart());                                      
58:                     layer.setQuery(definitionQuery);
59:                     mapContext.addLayer(layer);                    
60:         } else if(layers[i].getType().getCode() == LayerInfo.Type.RASTER.getCode()) {
61:           //
62:         }
63:         
64:         // default outputFormat to 'png' and mimeType to 'image/png'
65:         this.mapProducer = new AgsOwsMapProducer();
66:         this.mapProducer.setMapContext(this.mapContext);
67:         
68:         this.mapProducer.produceMap();
69:         this.mapProducer.writeTo(output);
70:       }
71:     } catch(Exception e) {
72:       e.printStackTrace();
73:     }
74:   }
75: 
And AgsOwsMapProducer, which takes the map context, finally renders a map using GeoTools StreamingRenderer by calling produceMap() and write out image stream by calling writeTo().
1: ...
2: public void produceMap() throws Exception {  
3:     ...
4:     Rectangle paintArea 
5:         = new Rectangle(0, 0, mapContext.getMapWidth(), mapContext.getMapHeight());
6:     String antialias = "none";
7:     IndexColorModel palette = null;
8:     ...
9:     boolean useAlpha = true;
10:     final boolean transparent = mapContext.isTransparent();
11:     ...
12:     final Color bgColor = mapContext.getBgColor();
13:     ...
14:     final RenderedImage preparedImage 
15:         = ImageUtils.createImage(paintArea.width, paintArea.height, palette, useAlpha);
16:     ...
17:     final Map<RenderingHints.Key, Object> hintsMap 
18:         = new HashMap<RenderingHints.Key, Object>();      
19:     final Graphics2D graphic 
20:         = ImageUtils.prepareTransparency(transparent, bgColor, preparedImage, hintsMap);
21:     ...
22:     graphic.setRenderingHints(hintsMap);
23:     ...
24:     StreamingRenderer renderer = new StreamingRenderer();      
25:     renderer = new StreamingRenderer();        
26:     renderer.setContext(mapContext);
27:       
28:     RenderingHints hints = new RenderingHints(hintsMap);
29:     renderer.setJava2DHints(hints);
30:               
31:     Map<Object, Object> rendererParams = new HashMap<Object, Object>();
32:     rendererParams.put("optimizedDataLoadingEnabled", new Boolean(true));    
33:     rendererParams.put("maxFiltersToSendToDatastore",  DefaultWebMapService.getMaxFilterRules());
34:     rendererParams.put(ShapefileRenderer.SCALE_COMPUTATION_METHOD_KEY, ShapefileRenderer.SCALE_OGC);    
35:     ...
36:     rendererParams.put(StreamingRenderer.ADVANCED_PROJECTION_HANDLING_KEY, true);
37:     ...
38:     renderer.setRendererHints(rendererParams);
39:     try {
40:         final ReferencedEnvelope dataArea = mapContext.getAreaOfInterest();
41:         renderer.paint(graphic, paintArea, dataArea);        
42:     } finally {
43:         graphic.dispose();
44:     }
45:     ...
46:     this.image = preparedImage;
47: }
48: ...
49: public void writeTo(OutputStream out) throws ServiceException, java.io.IOException {
50:     ...
51:     final String format = getOutputFormat();
52:     ...
53:     new ImageWorker(image).writePNG(
54:         outStream, 
55:         "FILTERED", 
56:         100.0f,  
57:         true,
58:         image.getColorModel() instanceof IndexColorModel
59:     );
60:     ...
61: }
Note: the sample above for both produceMap() and writeTo() omitted a lot of details which can not be simply copied and paste to be compiled. But I will provide the link to download the sample code in the end of the series.

Wrap it up

So far I’ve finished everything I planned for this little “ags-ows” project. So just restart GeoServer and the customized OWS service “ags-ows” will be ready to serve out map. Here are two sample requests and result maps:

http://localhost:8080/geoserver/ows?service=agsows&request=export&version=1.0.0&bbox=-180,0,0,90&transparent=true&size=512,256&bboxSR=4326&imageSR=4326&layers=show:states
wgs84
http://localhost:8080/geoserver/ows?service=agsows&request=export&version=1.0.0&bbox=-20037508.34,-20037508.34,20037508.34,20037508.34&transparent=true&size=512,512&bboxSR=900913&imageSR=900913&layers=show:states
mercator

This is the final part of the whole series and the source code covered in my little "ags-ows" project is packaged as geoserver-playground-ags-ows.zip, which can be downloaded from here. Feel free to leave any feedback and comments.

3 comments:

  1. Thank you for this very interesting and helpful series. I was wondering if its possible to get the code, the link above does not seem to be working. Thank you again.

    ReplyDelete
  2. Sorry for the late reply, the link should be back live again. Give it a try!

    ReplyDelete
  3. Link is not working again. Is there a possibility to get the code? I'd be very grateful :)
    Interesting tutorial, ows still needs more tuts and documentations.

    ReplyDelete