============================================================================================================== GeoExt Browser Client ============================================================================================================== `GeoExt `_ is a *JavaScript Toolkit for Rich Web Mapping Applications*. GeoExt brings together the geospatial know how of `OpenLayers `_ with the user interface savvy of `Ext JS `_ to help you build powerful desktop style GIS apps on the web with JavaScript. Let's start with a simple GeoExt example and extend it with routing functionality then: .. literalinclude:: ../../web/routing-0.html :language: html In the header we include all the javascript and css needed for the application, we also define a function to be run when the page is loaded (Ext.onReady). This function creates a `GeoExt.MapPanel `_ with an OpenStreetMap layer centered to Denver. In this code, no OpenLayers.Map is explicitly created; the GeoExt.MapPanel do this under the hood: it takes the map options, the center and the zoom and create a map instance accordingly. To allow our users to get directions, we need to provide: * a way to select the routing algorithm (Shortest path Dijkstra, A* or Shooting*), * a way to select the start and final destination. .. note:: This chapter only show code snippets, the full source code of the page can be found in ``pgrouting-workshop/web/routing-final.html`` that should be on your desktop. The full listing can also be found at the end of this chapter. ------------------------------------------------------------------------------------------------------------- Routing method selection ------------------------------------------------------------------------------------------------------------- To select the routing method, we will use an `Ext.form.ComboBox `_: it behaves just like a normal html select but we can more easily control it. Just like the GeoExt.MapPanel, we need an html element to place our control, let's create a new div in the body (with 'method' as id): .. code-block:: html
Then we create the combo itself: .. code-block:: js var method = new Ext.form.ComboBox({ renderTo: "method", triggerAction: "all", editable: false, forceSelection: true, store: [ ["SPD", "Shortest Path Dijkstra"], ["SPA", "Shortest Path A*"], ["SPS", "Shortest Path Shooting*"] ] }); In the ``store`` option, we set all the possible values for the routing method; the format is an array of options where an option is in the form ``[key, name]``. The ``key`` will be send to the server (the php script in our case) and the ``value`` displayed in the combo. The ``renderTo`` specify where the combo must be rendered, we use our new div here. And finally, a default value is selected: .. code-block:: js method.setValue("SPD"); This part only uses ExtJS component: no OpenLayers or GeoExt code here. ------------------------------------------------------------------------------------------------------------- Select the start and final destination ------------------------------------------------------------------------------------------------------------- We want to allow the users to draw and move the start and final destination points. This is more or less the behavior of google maps and others: the user selects the points via a search box (address search) or by clicking the map. The system query the server and display the route on the map. The user can later move the start or final point and the route is updated. In this workshop, we will only implement the input via the map (draw points and drag-and-drop) but it's possible to implement the search box feature by using a web service like `GeoNames `_ or any other `geocoding `_ service. To do this we will need a tool to draw points (we will use the `OpenLayers.Control.DrawFeatures `_ control) and a tool to move points (`OpenLayers.Control.DragFeatures `_ will be perfect for this job). As their name suggests these controls comes from OpenLayers. These two controls will need a place to draw and manipulate the points; we will also need an `OpenLayers.Layer.Vector `_ layer. In OpenLayers, a vector layer in a place where features (a geometry and attributes) can be drawn programmatically (in contrast, the OSM layer is a raster layer). Because vector layers are cheap, we will use a second one to draw the route returned by the web service. The layers initialization is: .. code-block:: js // create the layer where the route will be drawn var route_layer = new OpenLayers.Layer.Vector("route", { styleMap: new OpenLayers.StyleMap(new OpenLayers.Style({ strokeColor: "#ff9933", strokeWidth: 3 })) }); ``"route"`` is the layer name, any string can be used. ``styleMap`` gives the layer a bit of visual style with a custom stroke color and width (in pixel). The second layer initialization is simply: .. code-block:: js // create the layer where the start and final points will be drawn var points_layer = new OpenLayers.Layer.Vector("points"); The two layers are added to the OpenLayers.Map object with: .. code-block:: js // add the layers to the map map.addLayers([points_layer, route_layer]); Let's look at the control to draw the points: because this component has special behavior it's more easy to create a new class based on the standard OpenLayers.Control.DrawFeatures control. This new control (named DrawPoints) is saved in a separated javascript file (``web/DrawPoints.js``): .. literalinclude:: ../../web/DrawPoints.js :language: js In the ``initialize`` function (that's the class constructor) we set that this control can only draw points (handler variable is OpenLayers.Handler.Point). The special behavior is implemented in the ``drawFeature`` function: because we only need the start and final points the control deactivates itself when two points are drawn by counting how many features has the vector layer. Control deactivation is ``this.deactivate()``. Our control is then created with: .. code-block:: js // create the control to draw the points (see the DrawPoints.js file) var draw_points = new DrawPoints(points_layer); ``points_layer`` is the vector layer created earlier. And now for the DragFeature control: .. code-block:: js // create the control to move the points var drag_points = new OpenLayers.Control.DragFeature(points_layer, { autoActivate: true }); Again, ``points_layer`` is the vector layer, ``autoActivate: true`` tells OpenLayers that we want this control to be automatically activated. .. code-block:: js // add the controls to the map map.addControls([draw_points, drag_points]); Adds the controls to the map. ------------------------------------------------------------------------------------------------------------- Call and receive data from web service ------------------------------------------------------------------------------------------------------------- The basic workflow to get a route from the web server is: #. transform our points coordinates from EPSG:900913 to EPSG:4326 #. call the web service with the correct arguments (method name and two points coordinates) #. parse the web service response: transform GeoJSON to OpenLayers.Feature.Vector #. transform all the coordinates from EPSG:4326 to EPSG:900913 #. add this result to a vector layer The first item is something new: our map uses the EPSG:900913 projection (because we use an OSM layer) but the web service expects coordinates in EPSG:4326: we have to re-project the data before sending them. This is not a big deal: we will simply use the `Proj4js `_ javascript library. (The second item *call the web service* is covered in the next chapter.) The routing web service in pgrouting.php returns a `GeoJSON `_ FeatureCollection object. A FeatureCollection is simply an array of features: one feature for each route segment. This is very convenient because OpenLayers and GeoExt have all what we need to handle this format. To make our live even easier, we are going to use the GeoExt.data.FeatureStore: .. code-block:: js var store = new GeoExt.data.FeatureStore({ layer: route_layer, fields: [ {name: "length"} ], proxy: new GeoExt.data.ProtocolProxy({ protocol: new OpenLayers.Protocol.HTTP({ url: './php/pgrouting.php', format: new OpenLayers.Format.GeoJSON({ internalProjection: epsg_900913, externalProjection: epsg_4326 }) }) }) }); A store is simply a container to store informations: we can push data into and get it back. Let's explain all the options: ``layer``: the argument is a vector layer: by specifying a layer, the FeatureStore will automatically draw the data received into this layer. This is exactly what we need for the last item (*add this result to a vector layer*) in the list above. ``fields``: lists all the attributes returned along with the geometry: pgrouting.php returns the segment length so we set it here. Note that this information is not used in this workshop. ``proxy``: the proxy item specify where the data should be taken: in our case from a HTTP server. The proxy type is GeoExt.data.ProtocolProxy: this class connects the ExtJS world (the store) and the OpenLayers world (the protocol object). ``protocol``: this OpenLayers component is able to make HTTP requests to an ``url`` (our php script) and to parse the response (``format`` option). By adding the ``internalProjection`` and ``externalProjection`` option, the coordinates re-projection in made by the format. We now have all what we need to handle the data returned by the web service: the next chapter will explain how and when to call the service. ------------------------------------------------------------------------------------------------------------- Trigger the web service call ------------------------------------------------------------------------------------------------------------- We need to call the web service when: * the two points are drawn * one of the point is moved * the routing method has changed Our vector layer generates an event (called ``featureadded``) when a new feature is added, we can listen to this event and call to pgrouting function (this function will be presented shortly): .. code-block:: js draw_layer.events.on({ featureadded: function() { pgrouting(store, draw_layer, method.getValue()); } }); .. note:: Before we continue some words about events: an event in OpenLayers (the same apply for ExtJS and other frameworks), is a system to allow a function to be called when *something* happened. For instance when a layer is added to the map or when the mouse is over a feature. Multiple functions can be connected to the same event. No event is generated when a point is moved, hopefully we can give a function to the DragFeature control to be called we the point is moved: .. code-block:: js drag_points.onComplete = function() { pgrouting(store, draw_layer, method.getValue()); }; For the *method* combo, we can add a listeners options to the constructor with a `select` argument (that's the event triggered when the user changes the value): .. code-block:: js var method = new Ext.form.ComboBox({ renderTo: "method", triggerAction: "all", editable: false, forceSelection: true, store: [ ["SPD", "Shortest Path Dijkstra"], ["SPA", "Shortest Path A*"], ["SPS", "Shortest Path Shooting*"] ], listeners: { select: function() { pgrouting(store, draw_layer, method.getValue()); } }); It's now time to present the pgrouting function: .. code-block:: js // global projection objects (uses the proj4js lib) var epsg_4326 = new OpenLayers.Projection("EPSG:4326"), epsg_900913 = new OpenLayers.Projection("EPSG:900913"); function pgrouting(store, layer, method) { if (layer.features.length == 2) { // erase the previous route store.removeAll(); // transform the two geometries from EPSG:900913 to EPSG:4326 var startpoint = layer.features[0].geometry.clone(); startpoint.transform(epsg_900913, epsg_4326); var finalpoint = layer.features[1].geometry.clone(); finalpoint.transform(epsg_900913, epsg_4326); // load to route store.load({ params: { startpoint: startpoint.x + " " + startpoint.y, finalpoint: finalpoint.x + " " + finalpoint.y, method: method } }); } } The pgrouting function calls the web service through the store argument. At first, the function checks if two points are passed in argument. Then, ``store.removeAll()`` is called to erase a previous result from the layer (remember that the store and the vector layer are binded). The two points coordinates are then projected using OpenLayers.Projection instances. Finally, ``store.load()`` is called with a ``params`` representing the pgrouting.php arguments (they are passed to the HTTP GET call). ------------------------------------------------------------------------------------------------------------- What's next ? ------------------------------------------------------------------------------------------------------------- Possible enhancements: * use a geocoding service to get start/final point * way point support * nice icons for the start and final points * driving directions (road map): we already have the segment length ------------------------------------------------------------------------------------------------------------- Full source code ------------------------------------------------------------------------------------------------------------- .. literalinclude:: ../../web/routing-final.html :language: html