• Tidak ada hasil yang ditemukan

Server-Side Clustering

To cluster data into common groups, you need to determine which points lay relatively close to each other, and then figure out how much clustering to apply to achieve the correct number of points. There are a variety of ways you can go about this, some simple and others much more complex. For the example here, we’ve chosen a simple method that we like to call the “grid” method.

To cluster using a grid, you take the outer boundary of the data set (for example the view- port), divide the area into equally sized grid cells, and then allocate each of your points to a cell. The size of the grid cells will determine how detailed the map data is. If you use a grid cell that is 100 pixels wide, then all markers within the 100-by-100 block will be combined into one marker. Listing 7-6 uses an incremental grid size starting with one-thirtieth of the longi- tude resolution:

$gridSize+=($nelng-$swlng)/30;

which increases if the total is still too large at the end of the loop:

if(count($clustered)>$limit) continue;

By incrementing the size of the cell, you can achieve the best resolution of data for the number of points available. Figure 7-5 shows an example map with grid cells and map areas outlined.

Figure 7-5. A map showing the marked grid cells used for clustering C H A P T E R 7■ O P T I M I Z I N G A N D S C A L I N G F O R L A R G E D ATA S E T S 162

Listings 7-6 and 7-7 (http://googlemapsbook.com/chapter7/ServerCluster/) are modified versions of the server-side boundary method.

Listing 7-6. Cluster Icon Additions to Improve the Server-Side Boundary Method JavaScript var map;

var centerLatitude = 42;

var centerLongitude = -72;

var startZoom = 10;

//create an icon for the clusters var iconCluster = new GIcon();

iconCluster.image = "http://googlemapsbook.com/chapter7/icons/cluster.png";

iconCluster.shadow = "http://googlemapsbook.com/chapter7/icons/cluster_shadow.png";

iconCluster.iconSize = new GSize(26, 25);

iconCluster.shadowSize = new GSize(22, 20);

iconCluster.iconAnchor = new GPoint(13, 25);

iconCluster.infoWindowAnchor = new GPoint(13, 1);

iconCluster.infoShadowAnchor = new GPoint(26, 13);

//create an icon for the pins var iconSingle = new GIcon();

iconSingle.image = "http://googlemapsbook.com/chapter7/icons/single.png";

iconSingle.shadow = "http://googlemapsbook.com/chapter7/icons/single_shadow.png";

iconSingle.iconSize = new GSize(12, 20);

iconSingle.shadowSize = new GSize(22, 20);

iconSingle.iconAnchor = new GPoint(6, 20);

iconSingle.infoWindowAnchor = new GPoint(6, 1);

iconSingle.infoShadowAnchor = new GPoint(13, 13);

function init() {

map = new GMap2(document.getElementById("map"));

map.addControl(new GSmallMapControl());

map.setCenter(new GLatLng(centerLatitude, centerLongitude), startZoom);

updateMarkers();

GEvent.addListener(map,'zoomend',function() { updateMarkers();

});

GEvent.addListener(map,'moveend',function() { updateMarkers();

});

}

function updateMarkers() {

//remove the existing points map.clearOverlays();

//create the boundary for the data to provide //initial filtering

var bounds = map.getBounds();

var southWest = bounds.getSouthWest();

var northEast = bounds.getNorthEast();

var getVars = 'ne=' + northEast.toUrlValue() + '&sw=' + southWest.toUrlValue()

//log the URL for testing

GLog.writeUrl('server.php?'+getVars);

//retrieve the points

var request = GXmlHttp.create();

request.open('GET', 'server.php?'+getVars, true);

request.onreadystatechange = function() { if (request.readyState == 4) {

var jscript = request.responseText;

var points;

eval(jscript);

//create each point from the list for (i in points) {

var point = new GLatLng(points[i].lat,points[i].lng);

var marker = createMarker(point,points[i].type);

map.addOverlay(marker);

} } }

request.send(null);

}

function createMarker(point, type) {

//create the marker with the appropriate icon if(type=='c') {

var marker = new GMarker(point,iconCluster,true);

} else {

var marker = new GMarker(point,iconSingle,true);

}

return marker;

}

window.onload = init;

C H A P T E R 7■ O P T I M I Z I N G A N D S C A L I N G F O R L A R G E D ATA S E T S 164

Listing 7-7. Cluster Additions to Improve the Server-Side Boundary Method PHP Script

<?php

//This script may require additional memory ini_set('memory_limit',8388608 * 10);

//retrieve the variables from the GET vars list($nelat,$nelng) = explode(',',$_GET['ne']);

list($swlat,$swlng) = explode(',',$_GET['sw']);

//clean the data

$nelng = (float)$nelng;

$swlng = (float)$swlng;

$nelat = (float)$nelat;

$swlat = (float)$swlat;

//connect to the database

require($_SERVER['DOCUMENT_ROOT'] . '/db_credentials.php');

$conn = mysql_connect("localhost", $db_name, $db_pass);

mysql_select_db("googlemapsbook", $conn);

/*

* Retrieve the points within the boundary of the map.

* For the FCC data, all the points are within the US so we

* don't need to worry about the meridian problem.

*/

$result = mysql_query(

"SELECT

longitude as lng,latitude as lat,struc_height,struc_elevation FROM

fcc_towers WHERE

(longitude > $swlng AND longitude < $nelng) AND (latitude <= $nelat AND latitude >= $swlat) ORDER BY

lat");

//extract all the points from the result into an array

$list = array();

$row = mysql_fetch_assoc($result);

while($row) {

//use 'm' to indicate this is a regular (m)arker

$list[] = array($row['lat'],$row['lng'],'m');

$row = mysql_fetch_assoc($result);

}

//close the SQL connection mysql_close($conn);

//limit to 30 markers

$limit = 30;

$gridSize = 0;

$listRemove = array();

while(count($list)>$limit) {

//grid size in pixels. if the first pass fails to reduce the //number of markers below the limit, the grid will increase //again and redo the loop.

$gridSize += ($nelng-$swlng)/30;

$clustered = array();

reset($list);

//loop through the $list and put each one in a grid square while(list($k,$v) = each($list)) {

//calculate the y position based on the latitude: $v[0]

$y = floor(($v[0]-$swlat)/$gridSize);

//calculate the x position based on the longitude: $v[1]

$x = floor(($v[1]-$swlng)/$gridSize);

//use the x and y values as the key for the array and append //the points key to the clustered array

$clustered["{$x},{$y}"][] = $k;

}

//check if we're below the limit and if not loop again if(count($clustered)>$limit) continue;

//reformat the list array

$listRemove = array();

while(list($k,$v) = each($clustered)) {

//only merge if there is more than one marker in a cell if(count($v)>1) {

//create a list of the merged markers

$listRemove = array_merge($listRemove,$v);

//add a cluster marker to the list

$clusterLat = $list[$v[0]][0];

$clusterLng = $list[$v[0]][1];

C H A P T E R 7■ O P T I M I Z I N G A N D S C A L I N G F O R L A R G E D ATA S E T S 166

//use 'c' to indicate this is a (c)luster marker

$list[] = array($clusterLat,$clusterLng,'c');

} }

//unset all the merged pins

//reverse to start with highest key rsort($listRemove);

while(list($k,$v) = each($listRemove)) { unset($list[$v]);

}

//we're done!

break;

}

reset($list);

$json = array();

while(list($key,$values) = each($list)) {

$i++;

$json[] = "p{$i}:{lat:{$values[0]},lng:{$values[1]},type:'{$values[2]}'}";

}

//echo back the JavaScript object header('content-type:text/plain;');

echo "var points = {\n\t".join(",\n\t",$json)."\n}";

?>

These are good starting points for your clustering script. To make it even better, you could make some improvements. For example, you could calculate an average position of the mark- ers within one grid cell so that the cluster marker better represents the actual location of the points in that cell. You could also develop an algorithm that would allow you to cluster based on relative positions, so only dense groups would cluster rather than the entire page.

The advantages of the cluster method are that it isn’t restricted to zoom levels and it works for any sized data set. Its disadvantage is that the data is clustered over possibly large areas, so you will still need to zoom in for more detail.