[63] | 1 | ============================================================================================================== |
---|
| 2 | GeoExt Browser Client |
---|
| 3 | ============================================================================================================== |
---|
| 4 | |
---|
| 5 | `GeoExt <http://www.geoext.org/>`_ is a *JavaScript Toolkit for Rich Web Mapping Applications*. GeoExt |
---|
| 6 | brings together the geospatial know how of `OpenLayers <http://www.openlayers.org>`_ with |
---|
| 7 | the user interface savvy of `Ext JS <http://www.sencha.com>`_ to help you build powerful desktop |
---|
| 8 | style GIS apps on the web with JavaScript. |
---|
| 9 | |
---|
| 10 | Let's start with a simple GeoExt example and extend it with routing functionality then: |
---|
| 11 | |
---|
| 12 | .. literalinclude:: ../../web/routing-0.html |
---|
| 13 | :language: html |
---|
| 14 | |
---|
| 15 | In the header we include all the javascript and css needed for the application, |
---|
| 16 | we also define a function to be run when the page is loaded (Ext.onReady). |
---|
| 17 | |
---|
| 18 | This function creates a `GeoExt.MapPanel |
---|
| 19 | <http://www.geoext.org/lib/GeoExt/widgets/MapPanel.html>`_ with an |
---|
| 20 | OpenStreetMap layer centered to Denver. In this code, no OpenLayers.Map is |
---|
| 21 | explicitly created; the GeoExt.MapPanel do this under the hood: it takes the map options, the |
---|
| 22 | center and the zoom and create a map instance accordingly. |
---|
| 23 | |
---|
| 24 | To allow our users to get directions, we need to provide: |
---|
| 25 | * a way to select the routing algorithm (Shortest path Dijkstra, A* or Shooting*), |
---|
| 26 | * a way to select the start and final destination. |
---|
| 27 | |
---|
| 28 | .. note:: This chapter only show code snippets, the full source code of the |
---|
| 29 | page can be found in ``pgrouting-workshop/web/routing-final.html`` that should |
---|
| 30 | be on your desktop. The full listing can also be found at the end of this chapter. |
---|
| 31 | |
---|
| 32 | ------------------------------------------------------------------------------------------------------------- |
---|
| 33 | Routing method selection |
---|
| 34 | ------------------------------------------------------------------------------------------------------------- |
---|
| 35 | |
---|
| 36 | To select the routing method, we will use an `Ext.form.ComboBox |
---|
| 37 | <http://www.sencha.com/deploy/dev/docs/?class=Ext.form.ComboBox>`_: it |
---|
| 38 | behaves just like a normal html select but we can more easily control it. |
---|
| 39 | |
---|
| 40 | Just like the GeoExt.MapPanel, we need an html element to place our control, |
---|
| 41 | let's create a new div in the body (with 'method' as id): |
---|
| 42 | |
---|
| 43 | .. code-block:: html |
---|
| 44 | |
---|
| 45 | <body> |
---|
| 46 | <div id="gxmap"></div> |
---|
| 47 | <div id="method"></div> |
---|
| 48 | </body> |
---|
| 49 | |
---|
| 50 | Then we create the combo itself: |
---|
| 51 | .. code-block:: js |
---|
| 52 | |
---|
| 53 | var method = new Ext.form.ComboBox({ |
---|
| 54 | renderTo: "method", |
---|
| 55 | triggerAction: "all", |
---|
| 56 | editable: false, |
---|
| 57 | forceSelection: true, |
---|
| 58 | store: [ |
---|
| 59 | ["SPD", "Shortest Path Dijkstra"], |
---|
| 60 | ["SPA", "Shortest Path A*"], |
---|
| 61 | ["SPS", "Shortest Path Shooting*"] |
---|
| 62 | ] |
---|
| 63 | }); |
---|
| 64 | |
---|
| 65 | In the ``store`` option, we set all the possible values for the routing method; |
---|
| 66 | the format is an array of options where an option is in the form ``[key, name]``. |
---|
| 67 | The ``key`` will be send to the server (the php script in our case) and the |
---|
| 68 | ``value`` displayed in the combo. |
---|
| 69 | |
---|
| 70 | The ``renderTo`` specify where the combo must be rendered, we use our new div here. |
---|
| 71 | |
---|
| 72 | And finally, a default value is selected: |
---|
| 73 | .. code-block:: js |
---|
| 74 | |
---|
| 75 | method.setValue("SPD"); |
---|
| 76 | |
---|
| 77 | This part only uses ExtJS component: no OpenLayers or GeoExt code here. |
---|
| 78 | |
---|
| 79 | ------------------------------------------------------------------------------------------------------------- |
---|
| 80 | Select the start and final destination |
---|
| 81 | ------------------------------------------------------------------------------------------------------------- |
---|
| 82 | |
---|
| 83 | We want to allow the users to draw and move the start and final destination |
---|
| 84 | points. This is more or less the behavior of google maps and others: the user |
---|
| 85 | selects the points via a search box (address search) or by clicking the |
---|
| 86 | map. The system query the server and display the route on the map. The user |
---|
| 87 | can later move the start or final point and the route is updated. |
---|
| 88 | |
---|
| 89 | In this workshop, we will only implement the input via the map (draw points and |
---|
| 90 | drag-and-drop) but it's possible to implement the search box feature by using a |
---|
| 91 | web service like `GeoNames <http://www.geonames.org/>`_ or any other `geocoding |
---|
| 92 | <http://en.wikipedia.org/wiki/Geocoding>`_ service. |
---|
| 93 | |
---|
| 94 | To do this we will need a tool to draw points (we will use the |
---|
| 95 | `OpenLayers.Control.DrawFeatures |
---|
| 96 | <http://openlayers.org/dev/examples/draw-feature.html>`_ control) and a tool to |
---|
| 97 | move points (`OpenLayers.Control.DragFeatures |
---|
| 98 | <http://openlayers.org/dev/examples/drag-feature.html>`_ will be perfect for |
---|
| 99 | this job). As their name suggests these controls comes from OpenLayers. |
---|
| 100 | |
---|
| 101 | These two controls will need a place to draw and manipulate the points; we |
---|
| 102 | will also need an `OpenLayers.Layer.Vector |
---|
| 103 | <http://dev.openlayers.org/releases/OpenLayers-2.10/doc/apidocs/files/OpenLayers/Layer/Vector-js.html>`_ |
---|
| 104 | layer. In OpenLayers, a vector layer in a place where features (a geometry and |
---|
| 105 | attributes) can be drawn programmatically (in contrast, the OSM layer is a |
---|
| 106 | raster layer). |
---|
| 107 | |
---|
| 108 | Because vector layers are cheap, we will use a second one to draw the route |
---|
| 109 | returned by the web service. The layers initialization is: |
---|
| 110 | |
---|
| 111 | .. code-block:: js |
---|
| 112 | |
---|
| 113 | // create the layer where the route will be drawn |
---|
| 114 | var route_layer = new OpenLayers.Layer.Vector("route", { |
---|
| 115 | styleMap: new OpenLayers.StyleMap(new OpenLayers.Style({ |
---|
| 116 | strokeColor: "#ff9933", |
---|
| 117 | strokeWidth: 3 |
---|
| 118 | })) |
---|
| 119 | }); |
---|
| 120 | |
---|
| 121 | ``"route"`` is the layer name, any string can be used. |
---|
| 122 | ``styleMap`` gives the layer a bit of visual style with a custom stroke color and |
---|
| 123 | width (in pixel). |
---|
| 124 | |
---|
| 125 | The second layer initialization is simply: |
---|
| 126 | |
---|
| 127 | .. code-block:: js |
---|
| 128 | |
---|
| 129 | // create the layer where the start and final points will be drawn |
---|
| 130 | var points_layer = new OpenLayers.Layer.Vector("points"); |
---|
| 131 | |
---|
| 132 | The two layers are added to the OpenLayers.Map object with: |
---|
| 133 | |
---|
| 134 | .. code-block:: js |
---|
| 135 | |
---|
| 136 | // add the layers to the map |
---|
| 137 | map.addLayers([points_layer, route_layer]); |
---|
| 138 | |
---|
| 139 | Let's look at the control to draw the points: because this component has |
---|
| 140 | special behavior it's more easy to create a new class based on the standard |
---|
| 141 | OpenLayers.Control.DrawFeatures control. This new control (named DrawPoints) is |
---|
| 142 | saved in a separated javascript file (``web/DrawPoints.js``): |
---|
| 143 | |
---|
| 144 | .. literalinclude:: ../../web/DrawPoints.js |
---|
| 145 | :language: js |
---|
| 146 | |
---|
| 147 | In the ``initialize`` function (that's the class constructor) we set that |
---|
| 148 | this control can only draw points (handler variable is OpenLayers.Handler.Point). |
---|
| 149 | |
---|
| 150 | The special behavior is implemented in the ``drawFeature`` function: because we |
---|
| 151 | only need the start and final points the control deactivates itself when two |
---|
| 152 | points are drawn by counting how many features has the vector |
---|
| 153 | layer. Control deactivation is ``this.deactivate()``. |
---|
| 154 | |
---|
| 155 | Our control is then created with: |
---|
| 156 | |
---|
| 157 | .. code-block:: js |
---|
| 158 | |
---|
| 159 | // create the control to draw the points (see the DrawPoints.js file) |
---|
| 160 | var draw_points = new DrawPoints(points_layer); |
---|
| 161 | |
---|
| 162 | ``points_layer`` is the vector layer created earlier. |
---|
| 163 | |
---|
| 164 | And now for the DragFeature control: |
---|
| 165 | |
---|
| 166 | .. code-block:: js |
---|
| 167 | |
---|
| 168 | // create the control to move the points |
---|
| 169 | var drag_points = new OpenLayers.Control.DragFeature(points_layer, { |
---|
| 170 | autoActivate: true |
---|
| 171 | }); |
---|
| 172 | |
---|
| 173 | Again, ``points_layer`` is the vector layer, ``autoActivate: true`` tells |
---|
| 174 | OpenLayers that we want this control to be automatically activated. |
---|
| 175 | |
---|
| 176 | .. code-block:: js |
---|
| 177 | |
---|
| 178 | // add the controls to the map |
---|
| 179 | map.addControls([draw_points, drag_points]); |
---|
| 180 | |
---|
| 181 | Adds the controls to the map. |
---|
| 182 | |
---|
| 183 | ------------------------------------------------------------------------------------------------------------- |
---|
| 184 | Call and receive data from web service |
---|
| 185 | ------------------------------------------------------------------------------------------------------------- |
---|
| 186 | |
---|
| 187 | The basic workflow to get a route from the web server is: |
---|
| 188 | |
---|
| 189 | #. transform our points coordinates from EPSG:900913 to EPSG:4326 |
---|
| 190 | #. call the web service with the correct arguments (method name and two points coordinates) |
---|
| 191 | #. parse the web service response: transform GeoJSON to OpenLayers.Feature.Vector |
---|
| 192 | #. transform all the coordinates from EPSG:4326 to EPSG:900913 |
---|
| 193 | #. add this result to a vector layer |
---|
| 194 | |
---|
| 195 | The first item is something new: our map uses the EPSG:900913 projection |
---|
| 196 | (because we use an OSM layer) but the web service expects coordinates in |
---|
| 197 | EPSG:4326: we have to re-project the data before sending them. This is not a |
---|
| 198 | big deal: we will simply use the `Proj4js <http://trac.osgeo.org/proj4js/>`_ |
---|
| 199 | javascript library. |
---|
| 200 | |
---|
| 201 | (The second item *call the web service* is covered in the next chapter.) |
---|
| 202 | |
---|
| 203 | The routing web service in pgrouting.php returns a `GeoJSON |
---|
| 204 | <http://geojson.org/>`_ FeatureCollection object. A FeatureCollection is simply |
---|
| 205 | an array of features: one feature for each route segment. This is very convenient because |
---|
| 206 | OpenLayers and GeoExt have all what we need to handle this format. To make our |
---|
| 207 | live even easier, we are going to use the GeoExt.data.FeatureStore: |
---|
| 208 | |
---|
| 209 | .. code-block:: js |
---|
| 210 | |
---|
| 211 | var store = new GeoExt.data.FeatureStore({ |
---|
| 212 | layer: route_layer, |
---|
| 213 | fields: [ |
---|
| 214 | {name: "length"} |
---|
| 215 | ], |
---|
| 216 | proxy: new GeoExt.data.ProtocolProxy({ |
---|
| 217 | protocol: new OpenLayers.Protocol.HTTP({ |
---|
| 218 | url: './php/pgrouting.php', |
---|
| 219 | format: new OpenLayers.Format.GeoJSON({ |
---|
| 220 | internalProjection: epsg_900913, |
---|
| 221 | externalProjection: epsg_4326 |
---|
| 222 | }) |
---|
| 223 | }) |
---|
| 224 | }) |
---|
| 225 | }); |
---|
| 226 | |
---|
| 227 | A store is simply a container to store informations: we can push data into and |
---|
| 228 | get it back. |
---|
| 229 | |
---|
| 230 | Let's explain all the options: |
---|
| 231 | |
---|
| 232 | ``layer``: the argument is a vector layer: by specifying a layer, the |
---|
| 233 | FeatureStore will automatically draw the data received into this |
---|
| 234 | layer. This is exactly what we need for the last item (*add this result to a |
---|
| 235 | vector layer*) in the list above. |
---|
| 236 | |
---|
| 237 | ``fields``: lists all the attributes returned along with the geometry: pgrouting.php |
---|
| 238 | returns the segment length so we set it here. Note that this information is not |
---|
| 239 | used in this workshop. |
---|
| 240 | |
---|
| 241 | ``proxy``: the proxy item specify where the data should be taken: in our case |
---|
| 242 | from a HTTP server. The proxy type is GeoExt.data.ProtocolProxy: this class |
---|
| 243 | connects the ExtJS world (the store) and the OpenLayers world (the protocol |
---|
| 244 | object). |
---|
| 245 | |
---|
| 246 | ``protocol``: this OpenLayers component is able to make HTTP requests to an ``url`` |
---|
| 247 | (our php script) and to parse the response (``format`` option). By adding the |
---|
| 248 | ``internalProjection`` and ``externalProjection`` option, the coordinates |
---|
| 249 | re-projection in made by the format. |
---|
| 250 | |
---|
| 251 | We now have all what we need to handle the data returned by the web service: the next |
---|
| 252 | chapter will explain how and when to call the service. |
---|
| 253 | |
---|
| 254 | ------------------------------------------------------------------------------------------------------------- |
---|
| 255 | Trigger the web service call |
---|
| 256 | ------------------------------------------------------------------------------------------------------------- |
---|
| 257 | |
---|
| 258 | We need to call the web service when: |
---|
| 259 | * the two points are drawn |
---|
| 260 | * one of the point is moved |
---|
| 261 | * the routing method has changed |
---|
| 262 | |
---|
| 263 | Our vector layer generates an event (called ``featureadded``) when a |
---|
| 264 | new feature is added, we can listen to this event and call to pgrouting |
---|
| 265 | function (this function will be presented shortly): |
---|
| 266 | |
---|
| 267 | .. code-block:: js |
---|
| 268 | |
---|
| 269 | draw_layer.events.on({ |
---|
| 270 | featureadded: function() { |
---|
| 271 | pgrouting(store, draw_layer, method.getValue()); |
---|
| 272 | } |
---|
| 273 | }); |
---|
| 274 | |
---|
| 275 | .. note:: Before we continue some words about events: an event in OpenLayers |
---|
| 276 | (the same apply for ExtJS and other frameworks), is a system to allow a |
---|
| 277 | function to be called when *something* happened. For instance when a layer is |
---|
| 278 | added to the map or when the mouse is over a feature. Multiple functions can |
---|
| 279 | be connected to the same event. |
---|
| 280 | |
---|
| 281 | No event is generated when a point is moved, hopefully we can give a |
---|
| 282 | function to the DragFeature control to be called we the point is moved: |
---|
| 283 | |
---|
| 284 | .. code-block:: js |
---|
| 285 | |
---|
| 286 | drag_points.onComplete = function() { |
---|
| 287 | pgrouting(store, draw_layer, method.getValue()); |
---|
| 288 | }; |
---|
| 289 | |
---|
| 290 | For the *method* combo, we can add a listeners options to the constructor with |
---|
| 291 | a `select` argument (that's the event triggered when the user changes the value): |
---|
| 292 | |
---|
| 293 | .. code-block:: js |
---|
| 294 | |
---|
| 295 | var method = new Ext.form.ComboBox({ |
---|
| 296 | renderTo: "method", |
---|
| 297 | triggerAction: "all", |
---|
| 298 | editable: false, |
---|
| 299 | forceSelection: true, |
---|
| 300 | store: [ |
---|
| 301 | ["SPD", "Shortest Path Dijkstra"], |
---|
| 302 | ["SPA", "Shortest Path A*"], |
---|
| 303 | ["SPS", "Shortest Path Shooting*"] |
---|
| 304 | ], |
---|
| 305 | listeners: { |
---|
| 306 | select: function() { |
---|
| 307 | pgrouting(store, draw_layer, method.getValue()); |
---|
| 308 | } |
---|
| 309 | }); |
---|
| 310 | |
---|
| 311 | |
---|
| 312 | It's now time to present the pgrouting function: |
---|
| 313 | |
---|
| 314 | .. code-block:: js |
---|
| 315 | |
---|
| 316 | // global projection objects (uses the proj4js lib) |
---|
| 317 | var epsg_4326 = new OpenLayers.Projection("EPSG:4326"), |
---|
| 318 | epsg_900913 = new OpenLayers.Projection("EPSG:900913"); |
---|
| 319 | |
---|
| 320 | function pgrouting(store, layer, method) { |
---|
| 321 | if (layer.features.length == 2) { |
---|
| 322 | // erase the previous route |
---|
| 323 | store.removeAll(); |
---|
| 324 | |
---|
| 325 | // transform the two geometries from EPSG:900913 to EPSG:4326 |
---|
| 326 | var startpoint = layer.features[0].geometry.clone(); |
---|
| 327 | startpoint.transform(epsg_900913, epsg_4326); |
---|
| 328 | var finalpoint = layer.features[1].geometry.clone(); |
---|
| 329 | finalpoint.transform(epsg_900913, epsg_4326); |
---|
| 330 | |
---|
| 331 | // load to route |
---|
| 332 | store.load({ |
---|
| 333 | params: { |
---|
| 334 | startpoint: startpoint.x + " " + startpoint.y, |
---|
| 335 | finalpoint: finalpoint.x + " " + finalpoint.y, |
---|
| 336 | method: method |
---|
| 337 | } |
---|
| 338 | }); |
---|
| 339 | } |
---|
| 340 | } |
---|
| 341 | |
---|
| 342 | The pgrouting function calls the web service through the store argument. |
---|
| 343 | |
---|
| 344 | At first, the function checks if two points are passed in |
---|
| 345 | argument. Then, ``store.removeAll()`` is called to erase a previous result |
---|
| 346 | from the layer (remember that the store and the vector layer are binded). |
---|
| 347 | The two points coordinates are then projected using OpenLayers.Projection |
---|
| 348 | instances. |
---|
| 349 | |
---|
| 350 | Finally, ``store.load()`` is called with a ``params`` representing the |
---|
| 351 | pgrouting.php arguments (they are passed to the HTTP GET call). |
---|
| 352 | |
---|
| 353 | ------------------------------------------------------------------------------------------------------------- |
---|
| 354 | What's next ? |
---|
| 355 | ------------------------------------------------------------------------------------------------------------- |
---|
| 356 | |
---|
| 357 | Possible enhancements: |
---|
| 358 | * use a geocoding service to get start/final point |
---|
| 359 | * way point support |
---|
| 360 | * nice icons for the start and final points |
---|
| 361 | * driving directions (road map): we already have the segment length |
---|
| 362 | |
---|
| 363 | ------------------------------------------------------------------------------------------------------------- |
---|
| 364 | Full source code |
---|
| 365 | ------------------------------------------------------------------------------------------------------------- |
---|
| 366 | |
---|
| 367 | .. literalinclude:: ../../web/routing-final.html |
---|
| 368 | :language: html |
---|
| 369 | |
---|