• No results found

4.4 Engine Design

4.4.2 Geospatial Searching

A key part of this engine’s function is the ability to perform searches from location data.

4.4.2.1 Location Retrieval

The location retrieval is achieved by one of the small amounts of JavaScript in application. This uses the HTML 5 geolocation API which attempts to retrieve the web browser’s location. As this is an HTML 5 standard, it works the same on any browser with geolocation capability (including mobile browsers).

This is the HTML element which the JavaScript manipulates by replacing the “href” attribute’s value with a generated URL based on a pattern which contains placeholders for the latitude and longitude values. The original URL and the pattern URL are generated using the Zend Framework URL generator (figure 4.19(a)), which means if the URL does change, the JavaScript should continue working.

(a)

<a id="locate-me" class="btn btn-primary btn-large" href="<?php

echo $this->url(array(

'action' => 'index',

'controller'=>'locate'

));

?>" data-geourl="<?php

echo $this->url(array(

'action'=>'geo',

'controller'=>'locate',

'lat' => ':lat:',

'lon' => ':lon:'

), 'geolocate');

?>">What's around me?</a>

(b)

<a id="locate-me" class="btn btn-primary btn-large"

href="http://lbsm-wiki.local/locate"

data-geourl="http://lbsm-wiki.local/geo/%3Alat%3A/%3Alon%3A">

What's around me?</a>

Figure 4.19: The PHP template followed by the HTML which is generated from the template

/geo/:lat:/:lon:, replaces the :lat: and :lon: placeholders with the actual latitude and longitude respectively. This is done by passing the HTML element (el) URL string (geoString) to a function called generateUrl.

var generateURL = function(el, geoString) {

if(navigator.geolocation) {

var watch = navigator.geolocation.watchPosition(

function(position) {

geoString = geoString.replace('%3Alat%3A', position.coords.latitude);

geoString = geoString.replace('%3Alon%3A', position.coords.longitude);

el.attr('href', geoString); }, null, {

enableHighAccuracy: true

}); }

}

Figure 4.20: Generating the geographic URL

Geolocating a web browser is not instant as it has to be permitted by the user, it will then attempt to obtain as accurate a location as possible, but this in itself takes time. This script continues to run and regenerate the URL every time a new or more accurate location is retrieved. If no location is retrieved by the time the link is clicked however, the original URL in the hrefattribute is followed which takes the user to a form. The contents of this form attempts to be auto-populated with the geolocation, however, if the user has not permitted this, they can type their geographic coordinates in manually as a last resort.

Whichever method is used, the end result is the user is redirected to a URL containing a set of geographic coordinates. This URL is for a page which uses

those coordinates to perform the search around that area.

4.4.2.2 Geolocation Storage

The storage of locations was designed using the concepts of a “many to many” relation, but not actually implemented as such. This is to keep the engine generic as it gives the ability for multiple posts to be at the same location, but those posts may be of any different “post type”. Furthermore, a location may not necessarily have a physical or static geographic location. Figure 4.21 shows the design of the two tables.

Figure 4.21: Locations and Locations to Posts Table

A row in the “locations” table must have a numeric ID and may have a geolocation which is stored as a Spatial Point data type. The “locations_has_posts” table contains a foreign key to the “locations” table called “locations_id”, and the other two columns denote the post type, and the post ID. These are both required as the posts are implemented by third parties, there is no guarantee that post IDs will be unique across post types.

4.4.2.3 Geospatial Query

The coordinates are retrieved from the URL for the geospatial query. The geospa- tial query is done through URL parameters rather than a JavaScript geolocation retrieval because this method enables a location to be bookmarked and also manu- ally called (such as by a third party application), thus increasing the flexibility of how this engine can be used. These coordinates are then used to create a bounding box (or Minimum Bound Rectangle) based on either a preset radius or a custom radius also passed in the URL.

The controller instantiates a new instance of the Application_Model_PostList

class which is then injected with a Location_Mbr object (see section 4.2.5 for more information). Once this has been set the method getPostsByGeoLocation

is called which calls the query, and gets the post IDs from the database.

The PHP method which performs the query is in theApplication_Model_DbTable_Locations

class and is called getLocationsFromMbr as show in figure 4.22, which is passed the Location_Mbr object.

public function getLocationsFromMbr(Location_Mbr $mbr) {

$polygon = $mbr->toPolygon()->toSql();

$sql = $this->select()

->from($this, array('id',

'g' => new Zend_Db_Expr('ASTEXT(geo)')))

->where('MBRContains(GeomFromText(?), geo) = 1', $polygon);

$locations = array();

foreach($this->fetchAll($sql) as $row) {

$loc = new Application_Model_Location();

$locations[$row['id']] = $loc->setDatabaseLocation(

$row['id'], $row['g'] );

}

return $locations; }

Figure 4.22: The getLocationsFromMbr method

This method converts the MBR (which contains the minimum and maximum longitudes and latitudes for a geographic location with a distance radius) to a

Location_Polygonobject which is then converted to a WKT (Well-Known Text) representation. A Zend_Db_Select object is used to construct the SQL select statement using the MBRContains MySQL function to return all the rows where the “point” is contained within the polygon. The SQL which is generated will be similar to figure 4.23.

SELECT id, ASTEXT(geo) FROM `locations` WHERE MBRContains(GeomFromText('POLYGON(( 53.499879484892 -2.2972739174845, 53.499879484892 -2.2504200825155, 53.472000515108 -2.2504200825155, 53.472000515108 -2.2972739174845, 53.499879484892 -2.2972739174845))'), geo) = 1

Figure 4.23: Example of generated SQL

This query converts each row to Application_Model_Location object. This object is used to convert handle change of location formats throughout the ap- plication, and contains the geographic location and the location ID (if it has one). This row is then returned and then the related rows in the “locations_has_ posts” are retrieved. The application now has a list of locations and posts in those loc- ations.

The post types are retrieved from the registry, and the getPosts methods are called from each, with the rowset, the locations array and MBR object being passed. Each of post types return an array of any number of Post objects. These arrays are then merged and stored in the post list object.

4.4.2.4 Sorting

Due to the retrieval being a bounding box query, the order in which they are returned is not the order they should be in (which is distance). The sorting could be achieved in the database query, but this is an incredibly inefficient process as

the database is not optimised to perform complex calculations.

This is all performed in the Application_Model_PostList::sortByDistance

method (figure 4.24). It uses the PHP usort function which implements a merge sort algorithm in which an anonymous function is passed as an argument to provide the search criteria. The criteria provided compares the distances of the posts from the centre of the bounding box. There will be occasions when multiple posts may be identical distances away, particularly as you can add multiple posts to the same location. If this is the case, then it is sorted by alphabetical order from the post’s title.

public function sortByDistance() { $pl = $this->_mbr->getLocation(); usort( $this->_postArray, function ($a, $b) use ($pl) {

$locationa = $a->getLocation()->getGeo();

$locationb = $b->getLocation()->getGeo();

if ($locationa == null) {

$distancea = 0; } else {

$distancea = $locationa->distanceTo($pl)->toKm(); }

if ($locationb == null) {

$distanceb = 0;l } else {

$distanceb = $locationb->distanceTo($pl)->toKm(); }

if ($distancea == $distanceb) {

return strcasecmp($a->getTitle(), $b->getTitle()); }

return ($distancea < $distanceb) ? -1 : 1; }

);

return $this; }

Figure 4.24: Sort the list of posts by distance method

Related documents