Openlayers Geojson Write My Essay

Thursday, October 10, 2013

OpenLayers & AngularJS - add features, choose their appearance and behaviour with clustering

Being a beginner in both OpenLayers and AngularJS it took me a long while to do this simple thing: add stuff on a map and make it show as I wanted. There were multiple gotchas and I intend to chronicle each and every one of those bastards.
First, while creating a map and doing all kinds of stuff with it using OpenLayers is a breeze, doing it "right" with AngularJS is not as simple. I thought I would not reinvent the wheel and looked for some integration of the two technologies and I found AzimuthJS. In order to add a map with Azimuth all you have to do is: <divol-mapcontrols="zoom,navigation,layerSwitcher,attribution,mousePosition"control-opts="{navigation:{handleRightClicks:true}}"><az-layername="Street"lyr-type="tiles"></az-layer><az-layername="Airports"lyr-type="geojson"lyr-url="examples/data/airports.json"projection="EPSG:4326"></az-layer></div> You may notice that it has a simple syntax, it offers the possibility of multiple layers and one of them is even loading features dynamically from a URL. Perfect so far.
First problem: the API that I am using is not in the GeoJSON format that Azimuth know how to handle and I cannot or will not change the API. I've tried a lot of weird crap, including adding a callback on the loadend layer event for a GeoJson layer in order to reparse the data and configure what I wanted. It all worked, but it was incredibly ugly. I've managed to add the entire logic in a Javascript file and do it all in that event. It wasn't any different from doing it from scratch in Javascript without any Angular syntax, though. So what I did was to create my own OpenLayers.Format. It wasn't so complicated, basically I inherited from OpenLayers.Format.JSON and added my own read logic. Here is the result:OpenLayers.Format.RSI = OpenLayers.Class(OpenLayers.Format.JSON, { read: function(json, type, filter) { type = (type) ? type : "FeatureCollection"; var results = null; var obj = null; if (typeof json == "string") { obj =, [json, filter]); } else { obj = json; } if(!obj) { OpenLayers.Console.error("Bad JSON: " + json); } var features=[]; for (var i=0; i<obj.length; i++) { var item=obj[i]; var point=new OpenLayers.Geometry.Point(item.Lon,item.Lat).transform('EPSG:4326', 'EPSG:3857'); if (!isNaN(point.x)&&!isNaN(point.y)) { var feature=new OpenLayers.Feature.Vector(point,item); features.push(feature); } } return features; }, CLASS_NAME: "OpenLayers.Format.RSI" }); All I had to do is load this in the page. But now the problem was that Azimuth only knows some types of layers based on a switch block. I've not refactored the code to be plug and play, instead I shamelessly changed it to try to use the GeoJson code with the format I provide as the lyr-type, if it exists in the OpenLayers.Format object. That settled that. By running the code so far I see the streets layer and on top of it a lot of yellow circles for each of my items.
Next problem: too many items. The map was very slow because I was adding over 30000 items on the map. I was in need of clustering. I wasted almost an entire day trying to figure out what it wouldn't work until I realised that it was an ordering issue. Duh! But still, in this new framework that I was working on I didn't want to add configuration in a Javascript event, I wanted to be able to configure as much as possible via AngularJS parameters. I noticed that Azimuth already had support for strategy parameters. Unfortunately it only supported an actual strategy instance as the parameter rather than a string. I had, again, to change the Azimuth code to first search for the name of the strategy parameters in OpenLayers.Strategy and if not found to $parse the string. Yet it didn't work as expected. The clustering was not engaging. Wasting another half an hour I realised that, at least in the case of this weirdly buggy Cluster strategy, I not only needed it, but also a Fixed strategy. I've changed the code to add the strategy instead of replacing it and suddenly clustering was working fine. I still have to make it configurable, but that is a detail I don't need to go into right now. Anyway, remember that the loadend event was not fired when only the Cluster strategy was in the strategies array of the layer; I think you need the Fixed strategy to load data from somewhere.
Next thing I wanted to do was to center the map on the features existent on the map. The map also needed to be resized to the actual page size. I added a custom directive to expand a div's height down to an element which I styled to be always on the bottom of the page. The problem now was that the map was getting instantiated before the div was resized. This means that maybe I had to start with a big default height of the div. Actually that caused a lot of problems since the map remained as big as first defined and centering the map was not working as expected. What was needed was a simple map.updateSize(); called after the div was resized. In order to then center and zoom the map on the existent features I used this code:var bounds={ minLon:1000000000, minLat:1000000000, maxLon:-1000000000, maxLat:-1000000000 }; for (var i=0; i<layer.features.length; i++) { var feature=layer.features[i]; var point=feature.geometry; if (!isNaN(point.x)&&!isNaN(point.y)) { bounds.minLon=Math.min(bounds.minLon,point.x); bounds.maxLon=Math.max(bounds.maxLon,point.x); bounds.minLat=Math.min(bounds.minLat,point.y); bounds.maxLat=Math.max(bounds.maxLat,point.y); } } map.updateSize(); var extent=new OpenLayers.Bounds(bounds.minLon,bounds.minLat,bounds.maxLon,bounds.maxLat); map.zoomToExtent(extent,true); Now, while the clustering was working OK, I wanted to show stuff and make those clusters do things for me. I needed to style the clusters. This is done via: layer.styleMap=new OpenLayers.StyleMap({ "default": defaultStyle, "select": selectStyle });{ "featureselected": clickFeature }); var; var hover = new OpenLayers.Control.SelectFeature( layer, {hover: true, highlightOnly: true} ); map.addControl(hover);{"featurehighlighted": displayFeature});{"featureunhighlighted": hideFeature}); hover.activate(); var click = new OpenLayers.Control.SelectFeature( layer, {hover: false} ); map.addControl(click); click.activate(); I am adding two OpenLayers.Control.SelectFeature controls on the map, one activates on hover, the other on click. The styles that are used in the style map define different colors and also a dynamic radius based on the number of features in a cluster. Here is the code:var defaultStyle = new OpenLayers.Style({ pointRadius: "${radius}", strokeWidth: "${width}", externalGraphic: "${icon}", strokeColor: "rgba(55, 55, 28, 0.5)", fillColor: "rgba(55, 55, 28, 0.2)" }, { context: { width: function(feature) { return (feature.cluster) ? 2 : 1; }, radius: function(feature) { return feature.cluster&&feature.cluster.length>1 ? Math.min(feature.attributes.count, 7) + 2 : 7; } } }); You see that the width and radius are defined as dynamic functions. But here we have an opportunity that I couldn't let pass. You see, in these styles you can also define the icons. How about defining the icon dynamically using canvas drawing and then toDataURL? And I did that! It's not really that useful, but it's really interesting:function fIcon(feature,type) { var iconKey=type+'icon'; if (feature[iconKey]) return feature[iconKey]; if(feature.cluster&&feature.cluster.length>1) { var canvas = document.createElement("canvas"); var radius=Math.min(feature.cluster.length, 7) + 2; canvas.width = radius*2; canvas.height = radius*2; var ctx = canvas.getContext("2d"); ctx.fillStyle = this.defaultStyle.fillColor; ctx.strokeStyle = this.defaultStyle.strokeColor; //ctx.fillRect(0,0,canvas.width,canvas.height); ctx.beginPath(); ctx.arc(radius,radius,radius,0,Math.PI*2); ctx.fill(); ctx.stroke(); ctx.fillStyle = this.defaultStyle.strokeColor; var bounds={ minX:1000000000, minY:1000000000, maxX:-1000000000, maxY:-1000000000 }; for(var c = 0; c < feature.cluster.length; c++) { var child=feature.cluster[c]; var x=feature.geometry.x-child.geometry.x; var y=feature.geometry.y-child.geometry.y; bounds.minX=Math.min(bounds.minX,x); bounds.minY=Math.min(bounds.minY,y); bounds.maxX=Math.max(bounds.maxX,x); bounds.maxY=Math.max(bounds.maxY,y); } var q=0; q=Math.max(Math.abs(bounds.maxX),q); q=Math.max(Math.abs(bounds.maxY),q); q=Math.max(Math.abs(bounds.minX),q); q=Math.max(Math.abs(bounds.minY),q); q=radius/q; var zoom=2; for(var c = 0; c < feature.cluster.length; c++) { var child=feature.cluster[c]; var x=-(feature.geometry.x-child.geometry.x)*q+radius; var y=(feature.geometry.y-child.geometry.y)*q+radius; ctx.fillRect(parseInt(x-zoom/2), parseInt(y-zoom/2), zoom, zoom); } feature[iconKey] = canvas.toDataURL("image/png"); } else { feature[iconKey] = OpenLayers.Marker.defaultIcon().url; } return feature[iconKey]; }; defaultStyle.context.icon=function(feature) { return,feature,'default'); } selectStyle.context.icon=function(feature) { return,feature,'select'); } This piece of code builds a map of the features in the cluster, zooms it to the size of the cluster icon, then also draws a translucent circle as a background.
I will not bore you with the displayFeature and clickFeature code, enough said that the first would set the html title on the layer element and the other would either zoom and center or display the info card for one single feature. There is a gotcha here as well, probably caused initially by the difference in size between the map and the layer. In order to get the actual pixel based on latitude and longitude you have to use map.getLayerPxFromLonLat(lonlat), not map.getPixelFromLonLat(lonlat). The second will work, but only after zooming or moving the map once. Pretty weird.

There are other issues that come to mind now, like making the URL for the data dynamic, based on specific parameters, but that's for another time.

As many of the networks that I am building as part of my involvement in the Infrastructure Transitions Research Consortium (ITRC – are inherently spatial, I began thinking about how it might be useful to be able to visualise a network using the underlying geography but also as an alternative, the underlying topology. I began exploring various tools, and libraries and just started playing around with D3 (  D3 is a javascript library that offers a wealth of widgets and out-of-the-box visualisations for all sorts of purposes. The gallery for D3 can be found here. As part of these out-of-the-box visualisations it is possible to create force directed layouts for network visualisation. At this stage I began to think about I can get my network data, created by using some custom Python modules, nx_pg and nx_pgnet and subsequently stored within a custom database schema within PostGIS (see previous post here for more details), in a format that D3 can cope with. The easiest solution was to export a network to JSON format as the nx_pgnet Python modules allow a user to export a networkx network to JSON format (NOTE: the following tables labelled “ratp_integrated_rail_stations” and “ratp_integrated_rail_routes_split” were created as ESRI Shapefiles and then read in to PostGIS using the “PostGIS Shapefile and DBF Loader”).

Example Code:

import os

import osgeo.ogr as ogr

import sys

import networkx as nx

from libs.nx_pgnet import nx_pg

from libs.nx_pgnet import nx_pgnet

conn = ogr.Open(“PG: host=’localhost’ dbname=’ratp_networks’ user='<a_user>’ password='<a_password>'”)

 #name of rail station table for nodes and edges

int_rail_node_layer = ‘ratp_integrated_rail_stations’

int_rail_edge_layer = ‘ratp_integrated_rail_routes_split’

 #read data from tables and create networkx-compatible network (ratp_intergrated_rail_network)

ratp_integrated_rail_network = nx_pg.read_pg(conn, int_rail_edge_layer, nodetable=int_rail_node_layer, directed=False, geometry_precision=9)

 #return some information about the network


 #write the network to network schema in PostGIS database

nx_pgnet.write(conn).pgnet(ratp_integrated_rail_network, ‘RATP_RAIL’, 4326, overwrite=True, directed=False, multigraph=False)

 #export network to json

nx_pgnet.export_graph(conn).export_to_json(ratp_integrated_rail_network, ‘<folder_path>’, ‘ratp_network’)


Having exported the network to JSON format (original data sourced from, this was then used as a basic example to begin to develop an interface using D3 to visualise the topological aspects, and OpenLayers to visualise the spatial aspects of the network. A simple starting point was to create a basic javascript file that contained lists of networks that can be selected within the interface and subsequently viewed. Not only did this include a link to the underlying file that contained the network data, but also references to styles that can be applied to the topological or geographic views of the networks. A series of separate styles using the underlying style regimes of D3 and OpenLayers were developed such that a style selectable in the topological view used exactly the same values for colours, stroke widths, fill colours as styles applicable in the geographic view.  These stylesheets, stored within separate javascript files are pulled in via an AJAX call using jQuery to the webpage, subsequently allowing a user to select them. Any numeric attributes or values attached at the node or edge level of each network could also subsequently be used as parameters to visualise the nodes or edges in the networks in either view e.g. change edge thickness, or node size, for example. Furthermore, any attribute at the node or edge level could be used for label values, and these various options are presented via a set of simple drop down menu controls on the right hand side of the screen. As you may expect, when a user is interested in the topological view, then only the topological style and label controls are displayed, and vice versa for the geographic view.

For spatial networks, the geographic aspects of the data are read from a “WKT” attribute attached to each node and edge, via a WKT format reader to create vector features within an OpenLayers Vector Layer. It is likely this will be extended such that networks can be loaded directly from those being served via WMS, such as through Geoserver, rather than loading many vector features on the client. However for the purposes of exploring this idea, all nodes and edges within the interface on the geographic view can be considered as vector features. The NodeID, Node_F_ID, and Node_T_ID values attached to each node, or edge respectively as a result of storing the data within the custom database schema, are used to define the network data within D3.

At this stage it is possible to view the topological or geographic aspects of the network within a single browser pane. Furthermore, if graph metrics have been calculated against a particular network and are attached at either the whole graph, or individual node or edge level, they too can be viewed within the interface via a series of tabs found towards the bottom. The following image represents an example of visualising the afore-mentioned Paris Rail network using the interface, where we can see that the controls mentioned, and how the same styles for the topological and geographic views are making it easier to understand where one node or edge resides within the two views. The next stage is to develop fully-linked views of a network such that selections made in one window are maintained within another. This type of tool can be particularly useful for finding disconnected edges via the topological view, and then finding out where that disconnected edge may exist in it’s true spatial location.

Example of the geographic view of the Paris Rail Network displayed using OpenLayers (data read in from JSON objects, with geometry as WKT)


Example of graph metrics (degree histogram) for Paris Rail Network (data stored at network level)


Example of the topological view of the Paris Rail Network displayed using force directed layouts from D3.js

This entry was posted in Research by David. Bookmark the permalink.


Leave a Reply

Your email address will not be published. Required fields are marked *