Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1238 lines (1034 sloc) 36.3 KB
<?php
// $Id$
function geoclustering_extract_coords_from_node(&$node,$geofield_name)
{
$location=$node->{$geofield_name}[0]['geo'];
//!!! switch to using content_format() once it starts working in Geo
// We must handle both WKB format (generated by geo_field('load'))
// and database-specific binary format (generated by
// geo_field('presave'))
if (substr($location,0,1) == '0') { // PostgreSQL; hex format
$location=pack('H*',$location);
}
elseif (substr($location,0,4) == pack(L,4326)) { // MySQL; skip SRID
$location=substr($location,4);
}
$point=geo_wkb_get_data($location,'array');
//!!! gracefully handle non-point geometries
$point=$point['value'];
return array(0 + $point['lon'],0 + $point['lat']);
}
function geoclustering_get_approximate_nr_of_nodes($coords,&$params)
{
$params_hash_sql=geoclustering_get_params_hash_sql($params);
geoclustering_ensure_uptodate_tree($params,$params_hash_sql);
list($dummy,$min_cluster_coords)=geoclustering_calc_cluster_idx(
array($coords[0],$coords[1]),$params);
list($dummy,$max_cluster_coords)=geoclustering_calc_cluster_idx(
array($coords[2],$coords[3]),$params);
$nr_of_clusters_in_area=1;
for ($i=0;$i < 2;$i++) {
$nr_of_clusters_in_area*=(2 * ($coords[$i]-$coords[2+$i])) /
(($min_cluster_coords[2+$i]-$min_cluster_coords[0+$i]) +
( $max_cluster_coords[2+$i]-$max_cluster_coords[0+$i]));
}
$level=$params->maxlevel - round(log($nr_of_clusters_in_area / 200,2));
$clusters_geo_handler=geo_load(array('name' => 'geoclustering_clusters'));
list($bbox_sql,$query_params)=geoclustering_get_bbox_sql(
$clusters_geo_handler,$coords);
$query_params[]=$level;
$query_params[]=$params_hash_sql;
return db_result(db_query(
"SELECT COALESCE(SUM(nr_of_nodes),0) " .
"FROM {geoclustering_clusters} " .
"WHERE " . $bbox_sql .
" AND level=%d AND params_hash=%s",
$query_params));
}
function geoclustering_calc_cluster_idx($coords,&$params)
{
/***********************************/
/***** *****/
/***** Calculate static tables *****/
/***** *****/
/***********************************/
static $geoclustering_pseudorandom_coeff;
static $geoclustering_cos_table;
if (!isset($geoclustering_pseudorandom_coeff)) {
$geoclustering_pseudorandom_coeff=array();
for ($lcs=0;count($geoclustering_pseudorandom_coeff) < 256;
$lcs=(($lcs * 0x6a85) + 37) & 32767) {
$value=1 + sqrt(($lcs >> 1)/16383) * 1.5 /*!!! make this configurable */;
if ($lcs & 1) {
$value=1.0 / $value;
}
$geoclustering_pseudorandom_coeff[]=$value;
}
$geoclustering_cos_table=array();
for ($i=-180;$i <= 180;$i++) {
$geoclustering_cos_table[$i]=1.0 / max(0.1,cos($i/2*M_PI/180));
}
}
/*******************************/
/***** *****/
/***** Normalize $coords[] *****/
/***** *****/
/*******************************/
if ($coords[0] < -180) {
$coords[0]=fmod($coords[0],360);
if ($coords[0] < -180) {
$coords[0]+=360;
}
}
elseif ($coords[0] >= 180) {
$coords[0]=fmod($coords[0],360);
if ($coords[0] >= 180) {
$coords[0]-=360;
}
}
/*********************/
/***** *****/
/***** Main loop *****/
/***** *****/
/*********************/
$min_lon=-180;
$min_lat=-90;
$max_lon=+180;
$max_lat=+90;
$lat_lon_ratio=0.5;
$cluster_idx=0;
for ($i=$params->maxlevel-1;$i >= 0;$i--) {
$ratio=$lat_lon_ratio *
$geoclustering_cos_table[(integer)($max_lat+$min_lat)];
// if ($i <= 10) { print (($ratio >= 1) ? $ratio : (1.0/$ratio)) ."\n"; }
if ($ratio >= $geoclustering_pseudorandom_coeff[$cluster_idx & 255]) {
$coord_threshold=( $min_lat +
$max_lat) / 2;
if ($coords[1] >= $coord_threshold) {
$min_lat=$coord_threshold;
$cluster_idx=($cluster_idx << 1) + 1;
}
else {
$max_lat=$coord_threshold;
$cluster_idx<<=1;
}
$lat_lon_ratio*=0.5;
}
else {
$coord_threshold=( $min_lon +
$max_lon) / 2;
if ($coords[0] >= $coord_threshold) {
$min_lon=$coord_threshold;
$cluster_idx=($cluster_idx << 1) + 1;
}
else {
$max_lon=$coord_threshold;
$cluster_idx<<=1;
}
$lat_lon_ratio*=2;
}
}
return array($cluster_idx,array($min_lon,$min_lat,
$max_lon,$max_lat));
}
function geoclustering_get_params_hash_sql($params)
{
$params_array=array($params->geofield_name,
$params->maxlevel,
$params->node_conditions,
$params->summed_field_names);
$hexdigest=substr(sha1(serialize($params_array)),0,8*2);
// Since PHP lacks a 64-bit integer type, this is a hack to
// generate an SQL expression evaluating to a 64-bit integer
$startval='';
if (hexdec($hexdigest[0]) >= 8) {
$startval='-9223372036854775808+';
$hexdigest[0]=dechex(hexdec($hexdigest[0]) - 8);
}
return $startval .
hexdec(substr($hexdigest,16-6,6)) . '+' .
hexdec(substr($hexdigest,16-2*6,6)) . '*4294967296/16+' .
hexdec(substr($hexdigest,0,4)) . '*281474976710656';
}
function geoclustering_test() //!!!
{
$params=array('maxlevel' => 22);
$results='';
for ($t=0;$t < 5;$t++) {
for ($i=-180;$i < 180;$i++) {
for ($j=-90;$j <= 90;$j++) {
$result=geoclustering_calc_cluster_idx(array($i,$j),$params);
if (!$t) {
$results.=" $result[0]";
}
}
}
}
print crc32($results) . "\n";
}
function geoclustering_params_and_coords_for_node(&$node)
{
$matching_params=array();
ctools_include('export');
foreach (ctools_export_load_object('geoclustering_tree_params')
as $params) {
$node_matches=TRUE;
if (isset($params->node_conditions)) {
foreach ($params->node_conditions as $field => $required_value) {
$value=is_array($node->{$field}) ?
$node->{$field}[0]['value'] : $node->{$field};
if ($value != $required_value) {
$node_matches=FALSE;
break;
}
}
}
if ($node_matches) {
$matching_params[geoclustering_get_params_hash_sql($params)]=
array($params,geoclustering_extract_coords_from_node(
$node,$params->geofield_name));
}
}
return $matching_params;
}
class geoclustering_table_lock {
public function __construct($write_tables,$read_tables=array())
{
// Input table names and aliases must be safe (no user
// input there) and in {...}
global $db_type;
foreach (array_keys($write_tables) as $alias) {
if (!$write_tables[$alias]) {
$write_tables[$alias]=$alias;
}
}
foreach (array_keys($read_tables) as $alias) {
if (!$read_tables[$alias]) {
$read_tables[$alias]=$alias;
}
}
if ($db_type == 'pgsql') {
$query="BEGIN; LOCK TABLE";
$delimiter=" ";
foreach (array_unique(array_values($write_tables)) as $table) {
$query.=$delimiter . $table;
$delimiter=",";
}
$query.=" IN ACCESS EXCLUSIVE MODE";
}
else {
// MySQL
// MySQL LOCK TABLES does not allow access to any other
// table than the locked tables, so we lock {watchdog}
// just in case to not disturb error logging
$write_tables['{watchdog}']='{watchdog}';
$query="LOCK TABLES";
$delimiter=" ";
foreach ($write_tables as $alias => $table) {
if ($alias != $table) {
$table.=" AS " . $alias;
}
$query.=$delimiter . $table . " WRITE";
$delimiter=",";
}
foreach ($read_tables as $alias => $table) {
if (!in_array($alias,$write_tables)) {
if ($alias != $table) {
$table.=" AS " . $alias;
}
$query.=$delimiter . $table . " READ";
$delimiter=",";
}
}
}
db_query($query);
}
public function __destruct()
{
global $db_type;
db_query(($db_type == 'pgsql') ? "COMMIT" : "UNLOCK TABLES");
}
};
/*!!! On non-MyISAM databases, use transactions and row-level locking
geoclustering_recalc_tree:
pgsql: BEGIN
pgsql: LOCK TABLE {geoclustering_clusters} IN EXCLUSIVE MODE
mysql: LOCK TABLE {geoclustering_clusters} WRITE,
{geoclustering_uptodate_trees} WRITE,
{node} ... READ
SELECT FROM node+joins
DELETE FROM {geoclustering_clusters}
INSERT INTO {geoclustering_clusters}
INSERT INTO {geoclustering_uptodate_trees} ... SELECT ...
pgsql: COMMIT
geoclustering_recalc_cluster optimised:
BEGIN
SELECT FROM node+joins
SELECT ... FROM {geoclustering_clusters} FOR UPDATE
if {
INSERT INTO {geoclustering_clusters}
}
UPDATE {geoclustering_clusters}
COMMIT
*/
function geoclustering_get_node_conditions(&$params)
{
$retval=array();
if (isset($params->node_conditions)) {
$node_schema=drupal_get_schema('node');
foreach ($params->node_conditions as $field => $required_value) {
// Check if the field is in {node} table
if (isset($node_schema['fields'][$field])) {
$table="node";
$column=$field;
$column_type=$node_schema['fields'][$field]['type'];
}
else {
// If it wasn't, then find the field from CCK
$dbinfo=content_database_info(content_fields($field));
$table=$dbinfo['table'];
foreach ($dbinfo['columns'] as $col => $colinfo) {
$column=$colinfo['column'];
$column_type=$colinfo['type'];
}
}
$retval[]=array('table' => $table,
'sql' => array("","." . $column . " = " .
db_type_placeholder($column_type)),
'param' => $required_value);
}
}
return $retval;
}
function geoclustering_get_summed_fields_info(&$params)
{
$retval=array();
foreach (($params->summed_field_names ?
$params->summed_field_names : array()) as $field) {
$dbinfo=content_database_info(content_fields($field));
if (!empty($dbinfo['columns'])) {
$colinfo=array_pop($dbinfo['columns']);
$retval[]=array($dbinfo['table'],$colinfo['column']);
}
}
return $retval;
}
function geoclustering_get_nodes_sql(&$params,$summed_field_function=NULL)
{
$geofield_dbinfo=content_database_info(content_fields(
$params->geofield_name));
//!!! mis returnitakse, kui mitmel content typel on sama field?
$geofield_table="{" . $geofield_dbinfo['table'] . "}";
$sql=" FROM {node} JOIN " . $geofield_table . " ON (" .
$geofield_table . ".vid={node}.vid)";
$joined_tables=array("{node}",$geofield_table);
/*******************************/
/***** *****/
/***** Add node_conditions *****/
/***** *****/
/*******************************/
$where_sql="";
$where_params=array();
foreach (geoclustering_get_node_conditions($params) as $clause) {
$prefixed_table="{" . $clause['table'] . "}";
if (!in_array($prefixed_table,$joined_tables)) {
$sql.=" JOIN " . $prefixed_table .
" ON (" . $prefixed_table . ".vid={node}.vid)";
$joined_tables[]=$prefixed_table;
}
if ($where_sql) {
$where_sql.=" AND ";
}
$where_sql.=implode($prefixed_table,$clause['sql']);
$where_params[]=$clause['param'];
}
if ($where_sql) {
$where_sql=" WHERE " . $where_sql;
}
/**********************************/
/***** *****/
/***** Add summed_field_names *****/
/***** *****/
/**********************************/
// This must be done after node_conditions because if they use
// the same field, the most restrictive JOIN (i.e. not LEFT JOIN)
// must be done
$select_fields_sql="";
$summed_field_idx=0;
foreach (geoclustering_get_summed_fields_info($params) as $info) {
list($table,$column)=$info;
$table="{" . $table . "}";
if (!in_array($table,$joined_tables)) {
$sql.=" LEFT JOIN " . $table .
" ON (" . $table . ".vid={node}.vid)";
$joined_tables[]=$table;
}
$column=$table . "." . $column;
if ($summed_field_function) {
$column=$summed_field_function . "(" . $column . ")";
}
$select_fields_sql.="," . $column .
" AS summed_field_" . $summed_field_idx;
$summed_field_idx++;
}
return array($select_fields_sql . $sql . $where_sql,
$where_params,
$joined_tables,
geo_load(array('table_name' => $geofield_dbinfo['table'])));
}
function geoclustering_polygon_sql($coords)
{
$params=array();
foreach (array(0,1,2,1,2,3,0,3,0,1) as $i) {
$params[]=$coords[$i];
}
return array("POLYGON((%f %f,%f %f,%f %f,%f %f,%f %f))",$params);
}
function geoclustering_get_bbox_sql($geo_handler,$coords)
{
list($wkt,$query_params)=geoclustering_polygon_sql($coords);
$query=$geo_handler->intersects($geo_handler,$wkt) . " AND " .
$geo_handler->x($geo_handler) . " < %f AND " .
$geo_handler->y($geo_handler) . " < %f";
$query_params[]=$coords[2];
$query_params[]=$coords[3];
return array($query,$query_params);
}
function geoclustering_summed_fields_from_row(&$row)
{
$prefix_len=strlen('summed_field_');
$summed_fields=array();
foreach ($row as $field => $value) {
if (!strpos($field,'summed_field_')) {
$summed_fields[(int)substr($field,$prefix_len)]=
$value + 0; // +0 casts to numeric type
}
}
ksort($summed_fields);
return $summed_fields;
}
function geoclustering_cast_to_numeric($a)
{
return $a + 0;
}
function geoclustering_numeric_sum($a,$b)
{
return $a + $b;
}
function geoclustering_add_to_clusters(&$clusters,$row,$key)
{
if (!isset($clusters[$key])) {
$clusters[$key]=$row;
}
else {
$clusters[$key][0]+=$row[0];
$clusters[$key][1]+=$row[1];
$clusters[$key][2]+=$row[2];
$clusters[$key][3]=min($row[3],$clusters[$key][3]);
$clusters[$key][4]=array_map('geoclustering_numeric_sum',
$clusters[$key][4],$row[4]);
}
}
function geoclustering_recalc_tree($params)
{
watchdog('geoclustering',"Recalculating tree '!treename'",
array('!treename' => $params->name),WATCHDOG_NOTICE);
$clusters_geo_handler=geo_load(array('name' => 'geoclustering_clusters'));
/******************************/
/***** *****/
/***** Read nodes from DB *****/
/***** *****/
/******************************/
$params_hash_sql=geoclustering_get_params_hash_sql($params);
list($sql_from,$query_params,$tables,$geo_handler)=
geoclustering_get_nodes_sql($params);
$txn=new geoclustering_table_lock(
array( '{geoclustering_clusters}' => '',
'{geoclustering_uptodate_trees}' => ''),
array_merge(array_combine($tables,$tables),
array('geoclustering_uptodate_trees_select' =>
'{geoclustering_uptodate_trees}',
'{geo}' => '')));
$r=db_query("SELECT " . $geo_handler->x($geo_handler) . " AS lon," .
$geo_handler->y($geo_handler) . " AS lat," .
"{node}.nid AS nid" .
$sql_from,
$query_params);
$clusters=array();
while ($row=db_fetch_array($r)) {
list($idx,$dummy)=geoclustering_calc_cluster_idx(
array($row['lon'],$row['lat']),$params);
geoclustering_add_to_clusters($clusters,
array($row['lon'],$row['lat'],1,$row['nid'],
geoclustering_summed_fields_from_row($row)),
$idx);
}
$insert_rows=array();
/*****************************************************/
/***** *****/
/***** Calculate parent clusters in cluster tree *****/
/***** *****/
/*****************************************************/
for ($level=$params->maxlevel;$level >= 0;$level--) {
$new_level_clusters=array();
foreach ($clusters as $idx => $cluster) {
$insert_rows[]=array( array( $cluster[0] / max(1,$cluster[2]),
$cluster[1] / max(1,$cluster[2])),
array( $cluster[2],
$cluster[3],
implode(' ',$cluster[4]),
$params_hash_sql,
$level,
$idx));
geoclustering_add_to_clusters($new_level_clusters,
$cluster,$idx>>1);
}
$clusters=$new_level_clusters;
}
/********************************/
/***** *****/
/***** Write clusters to DB *****/
/***** *****/
/********************************/
db_query("DELETE FROM {geoclustering_clusters} WHERE params_hash=%s",
$params_hash_sql);
foreach ($insert_rows as $row) {
//!!! optimise this by combining several INSERTs into one
db_query("INSERT INTO {geoclustering_clusters} " .
"(coords,nr_of_nodes,min_nid,summed_fields," .
"params_hash,level,idx) " .
"VALUES (" . $clusters_geo_handler->pointFromText(
'POINT(' . $row[0][0] . ' ' . $row[0][1] . ')') .
",%d,%d,'%s',%s,%d,%d)",
$row[1]);
}
/*********************************************/
/***** *****/
/***** Mark the tree as up-to-date in DB *****/
/***** *****/
/*********************************************/
// This is an atomic INSERT ... SELECT operation which works on
// both PostgreSQL and MySQL
db_query("INSERT INTO {geoclustering_uptodate_trees} " .
"(params_hash) SELECT params_hash FROM " .
"(SELECT %s AS params_hash) AS t WHERE NOT EXISTS " .
"(SELECT * FROM {geoclustering_uptodate_trees} " .
"AS geoclustering_uptodate_trees_select " .
"WHERE params_hash=%s)",
array($params_hash_sql,$params_hash_sql));
}
function geoclustering_ensure_uptodate_tree(&$params,$params_hash_sql)
{
$row=db_fetch_array(db_query("SELECT COUNT(*) AS cnt " .
"FROM {geoclustering_uptodate_trees} " .
"WHERE params_hash=%s",$params_hash_sql));
if (!$row['cnt']) {
//!!! After locking, re-check {geoclustering_uptodate_trees}
geoclustering_recalc_tree($params);
}
}
function geoclustering_recalc_cluster($cluster_idx,$cluster_coords,
&$params,$params_hash_sql)
{
/*****************************************/
/***** *****/
/***** Read $cluster_data from nodes *****/
/***** *****/
/*****************************************/
list($sql_from,$query_params,$tables,$geo_handler)=
geoclustering_get_nodes_sql($params,'SUM');
//!!! add weight to avg_* (also in parent cluster recalculation)
list($bbox_sql,$bbox_query_params)=geoclustering_get_bbox_sql(
$geo_handler,$cluster_coords);
$query="SELECT COUNT(*) AS nr_of_nodes,MIN({node}.nid) AS min_nid," .
"AVG(" . $geo_handler->x($geo_handler) . ") AS avg_longitude," .
"AVG(" . $geo_handler->y($geo_handler) . ") AS avg_latitude" .
$sql_from . " AND " . $bbox_sql;
$query_params=array_merge($query_params,$bbox_query_params);
$clusters_geo_handler=geo_load(array('name' => 'geoclustering_clusters'));
$txn=new geoclustering_table_lock(array('{geoclustering_clusters}' => ''),
array_combine($tables,$tables));
$cluster_data=db_fetch_array(db_query($query,$query_params));
$clusters_data_for_level=array();
$clusters_data_for_level[$params->maxlevel]=array(
$cluster_data['avg_longitude'] * $cluster_data['nr_of_nodes'],
$cluster_data['avg_latitude'] * $cluster_data['nr_of_nodes'],
$cluster_data['nr_of_nodes'],
$cluster_data['min_nid'],
geoclustering_summed_fields_from_row($cluster_data));
/*****************************************************************/
/***** *****/
/***** Read this cluster and its (possibly indirect) parents *****/
/***** *****/
/*****************************************************************/
$clusters_query="SELECT level,idx," .
$clusters_geo_handler->x($clusters_geo_handler) . " AS lon," .
$clusters_geo_handler->y($clusters_geo_handler) . " AS lat," .
"nr_of_nodes,min_nid,summed_fields " .
"FROM {geoclustering_clusters} " .
"WHERE params_hash=%s AND (level=0";
$clusters_query_params=array($params_hash_sql);
$idx=$cluster_idx;
for ($level=$params->maxlevel-1;$level >= 0;$level--) {
$idx>>=1;
$clusters_query.=" OR (level=%d AND idx >= %d AND idx <= %d)";
$clusters_query_params[]=$level+1;
$clusters_query_params[]=$idx*2;
$clusters_query_params[]=$idx*2+1;
}
$clusters_query.=")";
$cluster_exists=array();
$r=db_query($clusters_query,$clusters_query_params);
while ($row=db_fetch_array($r)) {
if ($row['idx'] ==
($cluster_idx >> ($params->maxlevel-$row['level']))) {
$cluster_exists[$row['level']]=TRUE;
}
else {
geoclustering_add_to_clusters($clusters_data_for_level,
array( $row['lon'] * $row['nr_of_nodes'],
$row['lat'] * $row['nr_of_nodes'],
$row['nr_of_nodes'],
$row['min_nid'],
array_map('geoclustering_cast_to_numeric',
explode(' ',$row['summed_fields']))),
$row['level']-1);
}
}
for ($level=$params->maxlevel-1;$level >= 0;$level--) {
geoclustering_add_to_clusters($clusters_data_for_level,
$clusters_data_for_level[$level+1],$level);
}
/**********************************************************/
/***** *****/
/***** Write clusters to geoclustering_clusters table *****/
/***** *****/
/**********************************************************/
//!!! Combine multiple INSERT/UPDATE/DELETE statements into one
foreach ($clusters_data_for_level as $level => $data) {
$where_clause="WHERE params_hash=%s AND level=%d AND idx=%d";
$cluster_selection_params=array($params_hash_sql,
$level,
$cluster_idx >> ($params->maxlevel-$level));
list($longitude,$latitude,$nr_of_nodes,$min_nid,$summed_fields)=$data;
$longitude/=max(1,$nr_of_nodes);
$latitude /=max(1,$nr_of_nodes);
$summed_fields=implode(' ',$summed_fields);
if (!$nr_of_nodes) {
db_query("DELETE FROM {geoclustering_clusters} " . $where_clause,
$cluster_selection_params);
}
else {
$query_params=array_merge(
array($nr_of_nodes,$min_nid,$summed_fields),
$cluster_selection_params);
$coords_sql=$clusters_geo_handler->pointFromText(
'POINT(' . $longitude . ' ' . $latitude . ')');
if (isset($cluster_exists[$level])) {
db_query("UPDATE {geoclustering_clusters} SET " .
"coords=" . $coords_sql .
",nr_of_nodes=%d,min_nid=%d,summed_fields='%s' " .
$where_clause,$query_params);
}
else {
db_query("INSERT INTO {geoclustering_clusters} " .
"(coords,nr_of_nodes,min_nid,summed_fields," .
"params_hash,level,idx) " .
"VALUES (" . $coords_sql . ",%d,%d,'%s',%s,%d,%d)",
$query_params);
}
}
}
}
function geoclustering_is_possible_input_node(&$node)
{ // Fast function to check whether the node could possibly be
// a Geoclustering input node, based on immutable parts of node only
$field_types=array_keys(geo_field_types());
foreach (content_fields(NULL,$node->type) as $field) {
if (in_array($field['type'],$field_types)) {
return TRUE;
}
}
return FALSE;
}
/**
* Implementation of hook_nodeapi()
*/
function geoclustering_nodeapi(&$node, $op)
{
if ($op == 'presave') {
if (!geoclustering_is_possible_input_node($node)) {
return;
}
// This is a hack to detect node data changes during node update.
// hook_nodeapi($op='presave') is invoked *before* node is
// updated in DB, so we load the previous data from DB
// at this point, and store them to
// $node->geoclustering_prev_params where they apparently
// cause no harm. Afterwards, hook_nodeapi($op='update')
// is invoked *after* node is updated in DB, so then we have
// both old data (in $node->geoclustering_prev_params) and
// new data (in $node) available.
$node->geoclustering_prev_params=array();
if (!empty($node->nid)) { // ignore inserts, process only updates
$node->geoclustering_prev_params=
geoclustering_params_and_coords_for_node(
node_load($node->nid));
}
}
elseif ($op == 'update' || $op == 'insert' || $op == 'delete') {
// For 'delete', 'presave' is not called and thus we have to
// set $node->geoclustering_prev_params here
if ($op == 'delete' && geoclustering_is_possible_input_node($node)) {
$node->geoclustering_prev_params=array();
}
if (!isset($node->geoclustering_prev_params)) {
return;
}
// Delete all clusters in DB which are no longer active.
// Otherwise they would be incorrectly considered up-to-date
// later, if they for some reason re-appear in exportables list
ctools_include('export');
$active_params_hash_sqls=array_map(
'geoclustering_get_params_hash_sql',
ctools_export_load_object('geoclustering_tree_params'));
$sql_where="";
if ($active_params_hash_sqls) {
$sql_where=" WHERE params_hash NOT IN (" .
implode(',',$active_params_hash_sqls) . ")";
}
$row=db_fetch_array(db_query("SELECT COUNT(*) AS cnt " .
"FROM {geoclustering_uptodate_trees}" . $sql_where));
if ($row['cnt']) {
db_query("DELETE FROM {geoclustering_uptodate_trees}" .
$sql_where);
db_query("DELETE FROM {geoclustering_clusters}" . $sql_where);
}
$clusters_recalculated=array();
foreach($node->geoclustering_prev_params as
$params_hash_sql => $info) {
list($params,$coords)=$info;
list($cluster_idx,$cluster_coords)=
geoclustering_calc_cluster_idx($coords,$params);
geoclustering_recalc_cluster($cluster_idx,$cluster_coords,
$params,$params_hash_sql);
$clusters_recalculated[$params_hash_sql]=$cluster_idx;
}
foreach (geoclustering_params_and_coords_for_node($node) as
$params_hash_sql => $info) {
list($params,$coords)=$info;
if (isset($node->geoclustering_prev_params[$params_hash_sql]) &&
$node->geoclustering_prev_params[$params_hash_sql][1] ==
$coords) {
continue; // Already recalculated this cluster
}
list($cluster_idx,$cluster_coords)=
geoclustering_calc_cluster_idx($coords,$params);
if (isset($clusters_recalculated[$params_hash_sql]) &&
$clusters_recalculated[$params_hash_sql] ==
$cluster_idx) {
continue; // Already recalculated this cluster
}
geoclustering_recalc_cluster($cluster_idx,$cluster_coords,
$params,$params_hash_sql);
}
}
}
/**
* Implementation of hook_flush_caches().
*/
function geoclustering_flush_caches()
{
db_query("TRUNCATE TABLE {geoclustering_uptodate_trees}");
db_query("TRUNCATE TABLE {geoclustering_clusters}");
return array();
}
/**
* Implementation of hook_views_api()
*/
function geoclustering_views_api()
{
return array('api' => 2);
}
function geoclustering_apply_query_replacements($query_replacements,$str)
{
foreach ($query_replacements as $pattern => $replacement) {
$str=preg_replace('|(?<![a-zA-Z0-9_])' .
preg_quote($pattern) . '(?![a-zA-Z0-9_])|',
$replacement,$str);
}
return $str;
}
function geoclustering_sql_concat($array)
{
global $db_type;
if ($db_type == 'mysql' || $db_type == 'mysqli') {
return "CONCAT(" . implode(",",$array) . ")";
}
else {
return implode(" || ",$array);
}
}
function geoclustering_get_cluster_info($tree_name,$views_cluster_id)
{
$matches=array();
if (!preg_match('/^c([0-9]+)_([0-9]+)$/',$views_cluster_id,$matches)) {
return FALSE;
}
list(,$level,$idx)=$matches;
$level=0 + $level;
$idx=0 + $idx;
if (!is_int($level) || !is_int($idx)) {
return FALSE;
}
ctools_include('export');
$ctools_objects=ctools_export_load_object('geoclustering_tree_params',
'names',array($tree_name));
if (empty($ctools_objects)) {
return FALSE;
}
$params=array_pop($ctools_objects);
$params_hash_sql=geoclustering_get_params_hash_sql($params);
geoclustering_ensure_uptodate_tree($params,$params_hash_sql);
$r=db_query("SELECT nr_of_nodes,summed_fields " .
"FROM {geoclustering_clusters} " .
"WHERE params_hash=%s AND level=%d AND idx=%d",
$params_hash_sql,$level,$idx);
$row=db_fetch_array($r);
if (!$row) {
return array();
}
$row_summed_fields=explode(' ',$row['summed_fields']);
$summed_fields=array();
foreach (($params->summed_field_names ?
$params->summed_field_names : array()) as $idx => $field) {
$summed_fields[$field]=0 + $row_summed_fields[$idx];
}
return array($row['nr_of_nodes'],$summed_fields);
}
/***************************************************************************/
/************************ *************************/
/************************ hook_views_query_alter() *************************/
/************************ *************************/
/***************************************************************************/
function geoclustering_views_query_alter(&$view,&$query)
{
//!!! do recalc check also when using geoclustering_clusters as base table
$tree_name=NULL;
$level=NULL;
foreach ($view->argument as $arg) {
if (isset($arg->geoclustering_tree_name)) {
// So this is a Geoclustering view that we should modify
$tree_name=$arg->geoclustering_tree_name;
$level=$arg->geoclustering_level;
}
}
if ($tree_name === NULL) {
return;
}
ctools_include('export');
$ctools_objects=ctools_export_load_object('geoclustering_tree_params',
'names',array($tree_name));
if (empty($ctools_objects)) {
watchdog('geoclustering',"View '!viewname' settings specify " .
"nonexistent Geoclustering tree name '!treename'",
array( '!viewname' => $view->name,
'!treename' => $tree_name),
WATCHDOG_ERROR);
drupal_not_found(); //!!! Find a better way to fail. This one
// still executes the Views DB query
return;
}
$params=array_pop($ctools_objects);
$params_hash_sql=geoclustering_get_params_hash_sql($params);
if ($level > $params->maxlevel) {
$level=-2;
}
if ($level != -2) { // Do this always except when argument specifies
// that view should always return nodes
geoclustering_ensure_uptodate_tree($params,$params_hash_sql);
/************************************************/
/***** *****/
/***** Get WHERE clauses from original View *****/
/***** *****/
/************************************************/
$where_clauses=array();
$where_args=array();
$geofield_dbinfo=content_database_info(content_fields(
$params->geofield_name));
$query_replacements=array(
$geofield_dbinfo['table'] => 'geoclustering_clusters',
$geofield_dbinfo['columns']['geo']['column'] => 'coords',
);
foreach ($query->table_queue as $id => $tabledef) {
if ($tabledef['join']->table == $geofield_dbinfo['table']) {
$query_replacements[$id]='geoclustering_clusters';
}
}
foreach ($query->where as $clauses) {
foreach ($clauses['clauses'] as $clause) {
$where_clauses[]=geoclustering_apply_query_replacements(
$query_replacements,$clause);
}
$where_args=array_merge($where_args,$clauses['args']);
}
}
if ($level == -1) { // Do this when argument specifies "level=auto"
$max_results=$view->pager['items_per_page'];
if (empty($max_results)) {
$max_results=1000;
}
/********************************************************/
/***** *****/
/***** Find max level that has <= $max_results rows *****/
/***** *****/
/********************************************************/
$level=0;
$this_level_clusters=1;
$this_level_nodes=1;
while ($level < $params->maxlevel &&
$this_level_clusters < $max_results) {
for ($level_increment=0;
$level+$level_increment+1 < $params->maxlevel &&
max($this_level_clusters,$level ? 20 : 0) <<
($level_increment+1) <= $max_results/1.3;) {
$level_increment++;
}
$level+=$level_increment;
$q="SELECT level,COUNT(*) AS nr_of_clusters," .
"SUM(nr_of_nodes) AS sum_nr_of_nodes " .
"FROM {geoclustering_clusters} " .
"AS geoclustering_clusters " .
"WHERE params_hash=%s AND level >= %d AND level <= %d";
foreach ($where_clauses as $clause) {
$q.=" AND (" . $clause . ")";
}
$q.=" GROUP BY level ORDER BY level";
$max_query_level=min($params->maxlevel,$level+1+4-1);
$r=db_query($q,array_merge(
array($params_hash_sql,$level+1,$max_query_level),
$where_args));
while ($row=db_fetch_array($r)) {
$level=$row['level'];
$this_level_clusters=$row['nr_of_clusters'];
$this_level_nodes=$row['sum_nr_of_nodes'];
if ($this_level_clusters >= $max_results) {
break;
}
}
if ($level < $max_query_level &&
$this_level_clusters < $max_results) {
$level=$max_query_level;
}
}
if ($this_level_clusters > $max_results) {
$level--;
}
if ($level >= $params->maxlevel &&
$this_level_nodes <= $max_results) {
$level=-2;
}
//!!! Issue a warning if query has components not supported by
// Geoclustering (groupby's, inner joins, extra filters,
// OR filters, distinct fields, count fields, or
// other aggregate fields)
}
if ($level == -2) {
//!!! Wildcards must be disabled in argument handler
/************************************************************/
/***** *****/
/***** Query is going to be from {node} table. *****/
/***** Add additional WHERE fields from node_conditions *****/
/***** *****/
/************************************************************/
foreach (geoclustering_get_node_conditions($params) as $clause) {
$query->add_where(NULL,
implode($query->ensure_table($clause['table']),
$clause['sql']),
$clause['param']);
}
//!!! add WHERE clause "geofield IS NOT NULL"
// Remove fields coming from dummy join of {node} to
// geoclustering_clusters table. Our dummy join handler
// generates no SQL for this join, and thus the fields must
// be removed too, to prevent SQL errors
foreach ($query->fields as $idx => $field_def) {
if ($field_def['table'] == 'geoclustering_clusters') {
$query->fields[$idx]['field']='NULL';
$query->fields[$idx]['table']='';
}
}
return;
}
/*******************************************************************/
/***** *****/
/***** Rewrite query to read from geoclustering_clusters table *****/
/***** *****/
/*******************************************************************/
$orig_fields=$query->fields;
$orig_table_queue=$query->table_queue;
// Create new views_query
$view->base_table='geoclustering_clusters';
$view->init_query();
// Add WHERE clauses
foreach ($where_clauses as $idx => $clause) {
$view->query->add_where(NULL,$clause,(!$idx) ? $where_args : array());
}
$view->query->add_where(NULL,"params_hash = %s",$params_hash_sql);
$view->query->add_where(NULL,"level = %d",$level);
// Add SELECT fields
$summed_fields_info=geoclustering_get_summed_fields_info($params);
foreach ($orig_fields as $field_def) {
if (empty($field_def['table'])) {
$field=geoclustering_apply_query_replacements(
$query_replacements,$field_def['field']);
if ($field != $field_def['field']) {
$view->query->add_field($field_def['table'],$field,
$field_def['alias']);
continue;
}
}
$table=$field_def['table'];
// $table might be an alias, so resolve it to real table name
if (isset($orig_table_queue[$table]['join']->table)) {
$table=$orig_table_queue[$table]['join']->table;
}
if ($table == 'node' && $field_def['field'] == 'nid') {
$view->query->add_field(NULL,
"(CASE WHEN geoclustering_clusters.nr_of_nodes = 1 " .
"THEN " . geoclustering_sql_concat(array(
"''","geoclustering_clusters.min_nid")) .
" ELSE " . geoclustering_sql_concat(array(
"'c'","geoclustering_clusters.level",
"'_'","geoclustering_clusters.idx")) .
" END)",
$field_def['alias']);
continue;
}
if ($table == 'geoclustering_clusters') {
$view->query->add_field($table,$field_def['field'],
$field_def['alias']);
continue;
}
$summed_fields_idx=array_search(array($table,$field_def['field']),
$summed_fields_info);
if ($summed_fields_idx !== FALSE) {
$field='geoclustering_clusters.summed_fields';
// This is ugly SQL compatible with MySQL and PostgreSQL,
// equivalent to PostgreSQL SPLIT_PART() and
// MySQL SUBSTRING_INDEX()
for ($i=0;$i < $summed_fields_idx;$i++) {
$field="SUBSTRING(" . $field . " FROM (POSITION(' ' IN " .
$field . ")+1))";
}
if ($summed_fields_idx < count($summed_fields_info)-1) {
$field="SUBSTRING(" . $field .
" FROM 1 FOR (POSITION(' ' IN " . $field . ")-1))";
}
$view->query->add_field(NULL,$field,$field_def['alias']);
continue;
}
$view->query->add_field($table,$field_def['field'],
$field_def['alias']);
}
}