Sencha Touch Extensions & Plugins

Ext.ux.touch. MapLoader : Dynamically load map points as you pan around a map

Sencha Touch Extensions & Plugins,Web Design & Development Blog 6 Comments

Overview

This plugin allows you to have your map dynamically load points of interest as the user pans around the map allowing you to minimize the amount of data you have to fetch from the server and minimize the number of markers on the map speeding up performance by only dealing with points that are within the map’s visible area.

Extract the ZIP file into the ‘examples’ directory of the Sencha Touch libary package.

Download - Ext.ux.touch.MapLoader (80kb)

Demo – Ext.ux.touch.MapLoader

This demo loads randomly generated points onto the map as you pan and zoom. The markers are numbered based on the batch that they were loaded in to give an idea of the plugin working.

The Plugin

The plugin monitors the user’s panning and zooming and triggers a load of the specified store or leaves you to implement your own loading functions by hooking into its ‘mapload’ event.

Configuration Options

units the units that you would like to deal in. This can be set to ‘miles‘ or ‘km‘. Defaults to ‘miles
bufferType how you would like the map’s buffer to operate. Possible values are ‘ratio‘ or ‘fixed‘. 

This property allows you to include a ‘buffer’ distance to be added to the visible area’s radius allowing extra points to be loaded that won’t be initially visible. This means that when the user pans there are points present immediately before the full load is complete, making the user experience much more seamless.

fixed: this means that the value of the ‘buffer‘ config is always used as the buffer distance no matter what zoom level the map is on.

ratio: by using this setting the value of the ‘buffer‘ config option (set between 0 and 1) is multiplied by the visible area’s radius to get a proportional distance outside the visible area to include in the load.

For example if you are at a zoom level which means a 1 mile radius is visible to the user and you have a ratio buffer value of 0.5 then you will load points within a circle of 1.5 miles. This is useful when you consider a huge change in zoom. If we had a fixed buffer value of 0.5 miles in the previous situation then that would be acceptable, however if the user zoomed out to a visible radius of 200 miles then we would load very few extra points to smooth the load when the user pans. If we had a ratio buffer of 0.25 (25%) then at a zoom level with 1 mile radius we would load 1.25 miles and at a zoom level with 200 mile radius we would load an extra 50 miles giving us much more points outside the visible area to make the pan-load smoother.

buffer the value that will be used when calculating the Buffer Radius. 

If bufferType is set to ‘ratio‘ then the value should be a decimal between 0-1 (equivalent to a percentage) or if bufferType is set to ‘fixed‘ then this can be any number in the units defined in the units config.

store if defined the store will be automatically loaded when needed by the plugin – it effectively hooks up the ‘mapload’ event and loads the store with the correct parameters. 

If left null, it will be ignored and you must define your loading implementation.

disabled when set to true, the plugin will be disabled and no dynamic loading will happen.Can be modified using the enable(), disable() or setDisabled(value) methods
loadInterval this config can be used to fire loads while a user pans rather than only at the end of the pan movement.The number of milliseconds between loads while the user is panning. 

Set to 0 for no intermediate loads to take place.

Events

One event is added to the plugin’s parent Ext.Map object. The ‘mapload‘ event is fired when the user finishes a pan movement (including pauses in movement, not just when the touch is removed) or, if the loadInterval is set, at the defined intervals. The event passes 5 parameters to its handlers:

centre the map’s current centre. An object in the form { lat: xx, lng: yy }
bounds the bounds of the map’s current visible area. An object in the form: 


{
    northeast: {
        lat: xx,
        lng: yy
    },
    southwest: {
        lat: xx,
        lng: yy
    }
}
boundingRadius the radius of the visible map. I.e. the distance from the centre point to the corners of the visible region. Measured in the units provided in the units config.
bufferRadius the radius that has been calculated based on the bufferType and buffer config options. This doesn’t get added to the boundingRadius and is applied on the server.
zoom the current zoom level of the map.

An Example

The plugin itself is very simple with the complexity coming in the implementation of the load and post-load logic (e.g. adding markers etc). I will describe an example using an AJAX request rather than defining a store.

Defining the plugin


{
    xtype: 'map',
    plugins: [new Ext.ux.touch.MapLoader({
        units: 'miles',
        bufferType: 'ratio',
        buffer: 0.25, // calculate 25% of the visible radius,
        loadInterval: 300 // do a load every 300 ms during panning
    })],
    centered: true,
    mapOptions: {
        center: new google.maps.LatLng(55.857809, -4.242511), // centre over Glasgow
        zoom: 15
    }
}

Elsewhere we hook into the Map’s new ‘mapload‘ event and fire an AJAX call to the server passing up all the event’s parameters. The server code (which we’ll look at in a second) responds with a list of lat/lng positions and then our JS adds these points to our map.

// code found within the mapload's event handler

Ext.Ajax.request({
    url: 'server/search.php',
    method: 'get',
    params: {
        centre: Ext.encode(centre),
        bounds: Ext.encode(bounds),
        boundingRadius: boundingRadius,
        bufferRadius: bufferRadius,
        zoom: zoom
    },
    success: function(response){
        var jsonResponse = Ext.decode(response.responseText);

        if (jsonResponse.result) {

            for (var i = 0; i < jsonResponse.result.length; i++) {

                var resultPos = jsonResponse.result[i];

                if (!this.markerExists(resultPos.lat, resultPos.lng)) { // custom method (see full example code) to ignore points we've already loaded

                    // cache the newest point so we don't add it in the future
                    this.markerCache.add({
                        lat: resultPos.lat,
                        lng: resultPos.lng
                    });

                    var pos = new google.maps.LatLng(resultPos.lat, resultPos.lng);

                    var marker = new google.maps.Marker({
                        map: this.map,
                        position: pos
                    });
                }
            }
        }
    },
    scope: this
});

The serverside code simply parses the parameters being passed up and inputs them into a SQL version of the Haversine Formula. It then returns the results as JSON to be consumed by our JS code.


// Get parameters from URL
$centre = json_decode(stripslashes($_GET["centre"]), true);
$bounds = json_decode(stripslashes($_GET["bounds"]), true);
$boundingRadius = $_GET["boundingRadius"];
$bufferRadius = $_GET["bufferRadius"];
$zoom = $_GET["zoom"];

// Opens a connection to a mySQL server

// Set the active mySQL database

// Search the rows in the markers table
$query = sprintf("SELECT lat, lng, ( 3959 * acos( cos( radians('%s') ) * cos( radians( lat ) ) * cos( radians( lng ) - radians('%s') ) + sin( radians('%s') ) * sin( radians( lat ) ) ) ) AS distance FROM markers2 HAVING distance < '%s' ORDER BY distance",
    mysql_real_escape_string($centre['lat']),
    mysql_real_escape_string($centre['lng']),
    mysql_real_escape_string($centre['lat']),
    mysql_real_escape_string($boundingRadius) + mysql_real_escape_string($bufferRadius)); // add boundingRadius and bufferRadius

$result = mysql_query($query);

if (!$result) {
    die("Invalid query: " . mysql_error());
}

$outputArray = array();

while($array = mysql_fetch_array($result)){
    $outputArray[] = $array;
}

echo json_encode(array("success" => true, "count" => count($outputArray), "result" => $outputArray));

To use the ‘store‘ config simply move the ‘success’ function defined above into the store’s load event handler and it will have the same effect.

And that’s us, you should see your points loading as you pan the map!
There are a couple points you must consider when using the plugin and when implementing your surounding code, these are:

  • Duplicate points – it is highly possible (and likely!) that the same point will be included in multiple loads so you must take this into consideration when processing the loaded points to stop markers being added twice.
  • Redundant markers – markers that are off screen are still there so you may want to implement some clean-up code to destroy these markers when not needed to speed up performance.

If you have any problems with using the plugin, have questions about how it works or have suggestions about ways we can make it better then please leave us a comment or drop us an email!

Extract the ZIP file into the ‘examples’ directory of the Sencha Touch libary package.

Download - Ext.ux.touch.MapLoader (80kb)

Demo – Ext.ux.touch.MapLoader

This demo loads randomly generated points onto the map as you pan and zoom. The markers are numbered based on the batch that they were loaded in to give an idea of the plugin working.

Share this

6 Comments to "Ext.ux.touch. MapLoader : Dynamically load map points as you pan around a map"

  1. Winalot

    May 9, 2011

    Hi Stuart,

    Great plugin, however I had to change centre.wa and centre.ya to centre.Ea and centre.Fa (same for bounds) before it would work.

    Perhaps this changed in the GMaps implementation?

    Keep up the great work!

    WP

  2. Stuart

    May 9, 2011

    Hi

    Thanks for the feedback! Coincidentally someone else reported this bug this morning also. I will release an update later today to fix it – as you say the centre.**s need updated. It turns out to be an oversight on my part with the Gmaps library, as there is actually ‘get’ methods available to avoid using those 2 character aliases.

    Cheers
    Stuart

  3. Stuart

    May 9, 2011

    I have uploaded a new version of the plugin that will resolve this issue. Thanks for the bug report Winalot (…and Brendan via email!).

    You can download it from the blog article or directly from here

    Cheers
    Stuart

  4. Winalot

    May 9, 2011

    Hi Stuart,

    Thanks for the update; all working perfectly!

    Regards,

    WP

  5. BrendanMc

    May 9, 2011

    Hi Stuart,

    I have seen this recently in the Google Maps directionsService API where say
    location.Ca, location.Da changes to location.Ea,location.Fa….argh :-(

    Hence your change to :
    lat: centre.lat(),
    lng: centre.lng()
    I presume these getters are available on all location properties?
    Get work, appreciate the code-sharing and explanations.

    Brendan

  6. Stuart

    May 9, 2011

    Yea I think they are available on all location objects – I didn’t realise they existed initially either so that’s why the original code used the seemingly unstable property names.

    Stuart

Leave a Comment