OpenLayers Advanced Clustering and Setting dynamic images for OpenLayer Styles via Ajax


Update: - Read the same at Medium for better readability; Blogger is not technology blogging friendly

JSFiddle Source for this Tutorial
Let us start with a simple OpenLayer map In the JSP or the HTML we have


<head>
<script src="http://openlayers.org/api/OpenLayers.js"></script>
<script type="text/javascript"
 src="<%=request.getContextPath()%>/js/maplister.js"></script>
<script type="text/javascript"
 src="<%=request.getContextPath()%>/js/jquery-1.7.2.min.js"></script>
<script src="http://maps.google.com/maps/api/js?v=3&amp;sensor=false"></script>
</head>

<body>
<div id="map-id" style="height:100%;width=100%"></div>
</body>
And on document load AUI().ready(function(X) { resize("scMapListerportlet", "#map-id"); var iconpath ="<%=request.getContextPath()%>/images"; LayerCreationModule.initOpenLayerMap(24.938622,60.170421,iconpath,OpenLayers); var url = '<%=ajaxCallResourceURL.toString()%>'; LayerCreationModule.setUrlForDynamicImage(url);

var initOpenLayerMap = function(lat,long,path,openLayerP){
  
  openLayerObj=openLayerP;
  _projection = new openLayerObj.Projection("EPSG:4326");
  _bounds = new openLayerObj.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34);


  var mapControls = [ new openLayerObj.Control.Navigation(),
                      new openLayerObj.Control.PanZoomBar(),
                      new openLayerObj.Control.ScaleLine(),
                      new openLayerObj.Control.LayerSwitcher(),
                      new openLayerObj.Control.MousePosition(),
                      new openLayerObj.Control.Attribution() ];
  iconpath=path;
  map = new openLayerObj.Map({
   div:"map-id",
   allOverlays: true,
   //projection : 'EPSG:3857', //Spherical Mercator, but no need to specify heree
   displayProjection: _projection,
   maxResolution:'auto',
   units: "degrees",
   controls : mapControls,
   eventListeners : {
    'zoomend' : adjustMarkerStyle
   }
  });

                //Add the OpenSteetMap and GoogleMap Baselayers
  var osm = new openLayerObj.Layer.OSM("OpenStreeMap");
 
  var gmap =  new openLayerObj.Layer.Google("Google Streets", 
    {type: google.maps.MapTypeId.ROADMAP, 
   sphericalMercator:true, 
   'maxExtent': _bounds,visibility:false});


  map.addLayers([osm,gmap]);

This will give you the open layers map with two base layers - Google Maps and OpenStreet Map selected. This is straight enough except for the fact that you need to understand a little about Map Projections 

OpenStreetMap, Google Maps, Yahoo Map etc use a projection called the Spherical Mercator projection. This treats the globe as a perfect sphere. Spherical Mercator has an official designation of EPSG:3857. You can convert a given longitude,latitude to the map projection using the OpenLayers method as shown below. For more details regarding projections kindly read http://docs.openlayers.org/library/spherical_mercator.html
_projection = new openLayerObj.Projection("EPSG:4326");

    var covertlatitudelong = function (lat, long) {

        var lonlatObject = new OpenLayer.LonLat(lat, long).transform(
        _projection, map.getProjectionObject() //Speherical Meracao
        );
        return lonlatObject;

    };

Layers and Style - Relationship

  Let us create a layer and find out how layer and Style are related

 summary_layer = new openLayerObj.Layer.Vector("SummaryLayer", {
            styleMap: smapCluster,
            renderers: ['Canvas', 'SVG'],
            strategies: [new openLayerObj.Strategy.Cluster({
                "distance": 20
            })]

Now the million dollar insight about manipulating layer is via the StyleMap. It is easy enough once you know that it is important; If you thought initially that it is something to do with Style and ignore it, then you are going to miss the bus;
      var smapCluster = new openLayerObj.StyleMap({
            'default': pointStyleforCluster,
                'select': new openLayerObj.Style({
                pointRadius: "${radius}",
                fillColor: "yellow"

            })
        });
What we do here is to create a Style map and tell that for 'default' rendering use the pointStyleforCluster and in-case someone selects the feature apply the style listed for 'select' So let us see the full gamut of Style (take a peek below). There are two ways of specifying 'Style'. One via Static attributes and one via Dynamic attributes. Take the case of pointRadius. One can set a static value to it say 50 , and set the fillColor attribute to 'blue'. Then any feature ( for the time being think about a feature as a point in the map) added to this layer will have a blue circle seen in the map; Now what if we want to change the color of the feature based on a Custom Attribute for a feature we need to specify it as a dynamic attribute like "${fcolor}" and then in the context section add a function that takes a feature as an argument and based on some logic or attribute in the feature returns a specific color
 //styling for drawing
        var pointStyleforCluster = new openLayerObj.Style({
            fill: false,
            pointRadius: "${radius}", //default shape that is drawn is a circle with this radisu
            externalGraphic: "${chartUrl}", //this could be any url that returns an image
           // strokeColor: "red",
            fillColor: "${fcolor}", //fill color of the circle
            strokeOpacity: 0,
            fillOpacity: 0.5,
            strokeWidth: "${strokeWidth}",
            label: "${label}",// Text that will be visible for the feature
            labelOutlineWidth:1,
            //fontWeight: "bold",
            //fontColor:  "#ffffff",
            fontColor:  "black",
            fontOpacity: "0.8",
            fontSize: "10px"
            // ...
        }, {
            context: {

               label: function(feature) {

     if(feature.cluster){

      var clustercount = getClusterCount(feature);
      return  clustercount.planningCount +"/" +
      clustercount.inerrorCount+ "/" + clustercount.onairCount;
     }
     else{
      return "";
     };

    },
                radius: function (feature) {
                    //var pix = 2;
                    if (feature.cluster) {
                        return adjustMarkerStyle();
                    } else {
                        return 100;
                    };

                },
                fcolor: function (feature) {

                    if (feature.cluster) {
                        var cc = getClusterCount(feature);
                        if (cc.attributeCount != 1) {//This means it is clustered
                            return "blue";
                        }
                        if (cc.planningCount === 1) {
                            return "#cf33e1";
                        }
                        if (cc.inerrorCount === 1) {
                            return "#ff0000";
                        }
                        if (cc.onairCount === 1) {
                            return "#2da725";
                        }

                    } else {
                        return "grey";
                    };
                },
                strokeWidth: function (feature) {

                    if (feature.cluster) {
                        return adjustMarkerStyle() * 2;
                    };
                },

                chartUrl: function (feature) {

                    //..Draw a dynamic chart
        });

Adding Feature to a Layer with a Custom Attribute

We can add either a simple point or a geometric shape to a Feature and then add set of Features to the Layer (to which the style map is associated). For example if we add a Point to a feature,then the radius of the style map associated with the Layer will be taken to draw a circle with the Point as the Center. Similarly if we create a Polygon and add it to the Feature and add the feature to the Layer , the fillColor of the Polygon will be the one associated in the Style
 var features = layer.features;//If the layer has features already store it temporarily.

 var polygon = OpenLayer.Geometry.Polygon.createRegularPolygon(point, 50, 22, 0);
        var feature = new openLayerObj.Feature.Vector(polygon, {
            fcolor: color,// this is a custom attribute of this feature
            name: "Polygon"
        });

        features.push(feature);
        layer.removeAllFeatures();// A Quirk in the OpenLayer, remove all and push all features at one go for clustering especially to work)
        layer.addFeatures(features);

//OR Adding a Point and Custom Attribute

var lonlatObject = covertlatitudelong(lat, long);
        var point = new OpenLayer.Geometry.Point(lonlatObject.lon, lonlatObject.lat);


var  customAttr = "plan";

 var feature2 = new openLayerObj.Feature.Vector(point, {
            customAttr: customAttr// this is a Custom Attribute of the Feature
       
        });

        var summaryfeatures = summary_layer.features;
        summary_layer.removeAllFeatures();
        summaryfeatures.push(feature2);

Where to access and use the Custom Attributes added to the Feature

Okay now since we have the custom attributes and features in place let us see how to use them. Before that let us take a look at Clustering because the example I have given is heavily based on that and the custom attributes are added to the Layer with Clustering. So it would be better to explain if first. A cluster strategy is added to the Layer. Below we have a distance based simple Cluster ; If two features in the Map are within 20 pixels of each other , at any given Zoom level then it will create one new feature to represent both the separate points
 summary_layer = new openLayerObj.Layer.Vector("SummaryLayer", {
            styleMap: smapCluster,
            renderers: ['Canvas', 'SVG'],
            strategies: [new openLayerObj.Strategy.Cluster({
                "distance": 20
            })]
Once clustering is active, every time the bound of the map gets invalidated by the user zooming or panning the features get redrawn and the functions in the context associated with the Style Map gets called. So suppose we have only one layer which is clustered- say the SummaryLayer, and we want to print a text which has the count of the number of types of custom attribute are present in a clustered layer. Let us see how we do it.
//Code snippet from the Context part of the StyleMap

  context: {

               label: function(feature) {

     if(feature.cluster){

      var clustercount = getClusterCount(feature);
      return  clustercount.planningCount +"/" +
      clustercount.inerrorCount+ "/" + clustercount.onairCount;
      
     }
     else{
      return "";
     };

    },

Here we see that we are calling a function and passing in the feature. Let us take a look at the the getClusterCount function. The feature.cluster[i].attributes.. will give the list of feature attributes that are going to be replaced. So if we have added any custom attribute it should be added to the feature.attributes . Otherwise the next time clustering is called your Custom attribute may not be there in the clustered feature. Next as we can see we are just accessing the value of the customAttr and incrementing a number to count it. This is returned back and printed in the label. This same information can be send to the server via the externalGraphics url, a graph created on the fly and a png image of the graph generated and send as response to GET request of the externalGraphics URL
 var getClusterCount = function (feature) {

        var clustercount = {};
        var planningcount = 0;
        var onaircount = 0;
        var inerrorcount = 0;

        for (var i = 0; i < feature.cluster.length; i++) {

            if (feature.cluster[i].attributes.cluster) {
            //THE MOST IMPORTANT LINE IS THE ONE BELOW, While clustering open layers removes the orginial feature layer with its own. So to get the attributes of the feature you have added add it to the openlayer created feature layer
                feature.attributes.customAttr = feature.cluster[i].attributes.customAttr;
                switch (feature.cluster[i].attributes.customAttr) {

                    case "plan":
                        // plan
                        planningcount = planningcount + 1;
                        break;
                    case "error":
                        // error
                        inerrorcount = inerrorcount + 1;
                        break;
                    case "air":
                        // onair
                        onaircount = onaircount + 1;
                        break;
                }
            };

        }
        clustercount.planningCount = planningcount;
        clustercount.inerrorCount = inerrorcount;
        clustercount.onairCount = onaircount;
        clustercount.attributeCount = feature.attributes.count;
        // console.log(clustercount);
        return clustercount;
    };

Code snippet for generating the Pie Graph image from data in the URL .
 public void serveResource(ResourceRequest resourceRequest,
   ResourceResponse resourceResponse) throws IOException,
   PortletException {

  try {

   System.out.println("server function called");

   System.out.println("MapLister AJAX - image request");

   String onair = resourceRequest.getParameter("onair");
   String inerror = resourceRequest.getParameter("inerror");
   String inplanning = resourceRequest.getParameter("inplanning");

   System.out.println("onair="+onair+" inerror="+inerror+" inplanning="+inplanning);

   int width = 155;
   int height = 155;
   resourceResponse.setContentType("image/png");
   JFreeChart chart=createChart(new Integer(onair),new Integer(inerror),new Integer(inplanning));
   OutputStream out=resourceResponse.getPortletOutputStream();
   ChartUtilities.writeChartAsPNG(out, chart, width, height,true,0);
  

  } catch (Exception e) {
   System.out.println("Exception caused "+ e);
  }

 }

Comments

alexcpn said…
Updated Link http://jsfiddle.net/alexcpn/518p59k4/

Popular posts from this blog

Long running Java process resource consumption monitoring , leak detection and GC tuning

Best practises - Selenium WebDriver/ Java

CORBA - C++ Client (ACE TAO) Java Server (JacORB) A simple example