forked from kamba4/sunders
445 lines
14 KiB
PHP
445 lines
14 KiB
PHP
<?php
|
|
error_reporting(0);
|
|
include $pathToWebFolder.'config.php';
|
|
|
|
define('MAX_POINTS_FOR_QUICKHULL', 3000);
|
|
|
|
class OsmPoint {
|
|
var $id;
|
|
var $lat;
|
|
var $lon;
|
|
|
|
function __construct($valId, $valLat, $valLon) {
|
|
$this->id = $valId;
|
|
$this->lat = $valLat;
|
|
$this->lon = $valLon;
|
|
}
|
|
}
|
|
|
|
function mergeNeighbor(&$clusterGrid, $posCell, $pos) {
|
|
global $divDiag2;
|
|
|
|
if (!array_key_exists($pos, $clusterGrid)) {
|
|
/* the neighbor is empty */
|
|
return;
|
|
}
|
|
|
|
$neighbor = $clusterGrid[$pos];
|
|
|
|
if ($neighbor['count'] == 0) {
|
|
/* The neighbor is merged to an other cell, yet */
|
|
return;
|
|
}
|
|
|
|
$cell = $clusterGrid[$posCell];
|
|
|
|
/* Calculate the (square of) distance from the cell to its neighbor */
|
|
$lonDist = ($cell['longitude']/$cell['count']) - ($neighbor['longitude']/$neighbor['count']);
|
|
$latDist = ($cell['latitude'] /$cell['count']) - ($neighbor['latitude'] /$neighbor['count']);
|
|
$dist = ($lonDist * $lonDist) + ($latDist * $latDist);
|
|
|
|
if ($dist < $divDiag2 / 10) {
|
|
$count = $cell['count'] + $neighbor['count'];
|
|
$clusterGrid[$posCell]['latitude'] = $cell['latitude'] + $neighbor['latitude'];
|
|
$clusterGrid[$posCell]['longitude'] = $cell['longitude'] + $neighbor['longitude'];
|
|
$clusterGrid[$posCell]['count'] = $count;
|
|
$clusterGrid[$posCell]['points'] = array_merge($cell['points'], $neighbor['points']);
|
|
/* Invalidate the merged neighbor */
|
|
$clusterGrid[$pos]['count'] = 0;
|
|
}
|
|
}
|
|
|
|
function mergeNeighborhood(&$clusterGrid, $pos) {
|
|
global $divWCount, $divHCount;
|
|
|
|
if ($clusterGrid[$pos]['count'] == 0) {
|
|
/* cell merged yet */
|
|
return;
|
|
}
|
|
|
|
$latLon = explode(',', $clusterGrid[$pos]['grid'] );
|
|
if ($latLon[0] > 0 && $latLon[1] < $divHCount - 1) {
|
|
if ($latLon[0] > 0) {
|
|
$posNeigh = ($latLon[0] - 1) . ',' . ($latLon[1] + 1);
|
|
mergeNeighbor($clusterGrid, $pos, $posNeigh);
|
|
}
|
|
|
|
$posNeigh = $latLon[0] . ',' . ($latLon[1] + 1);
|
|
mergeNeighbor($clusterGrid, $pos, $posNeigh);
|
|
|
|
if ($latLon[0] < $divWCount - 1) {
|
|
$posNeigh = ($latLon[0] + 1) . ',' . ($latLon[1] + 1);
|
|
mergeNeighbor($clusterGrid, $pos, $posNeigh);
|
|
}
|
|
}
|
|
|
|
if ($latLon[0] < $divWCount - 1) {
|
|
$posNeigh = ($latLon[0] + 1) . ',' . $latLon[1];
|
|
mergeNeighbor($clusterGrid, $pos, $posNeigh);
|
|
}
|
|
}
|
|
|
|
/* Recursive calculation of the quick hull algo
|
|
$isBottom : 1 if bottom point is searched, -1 if top point is searched */
|
|
function quickHullCalc(&$pointList, $count, $minPoint, $maxPoint, $isBottom) {
|
|
|
|
$msg = 'Quick count='.$count.', min=('.$minPoint->id.','.$minPoint->lat.','.$minPoint->lon.'), max=('.$minPoint->id.','.$minPoint->lat.','.$minPoint->lon.'), isBottom='.$isBottom;
|
|
|
|
$farthestPoint = null;
|
|
$farthestDist = 0;
|
|
$outsideList = array();
|
|
$outsideListCount = 0;
|
|
|
|
if ($maxPoint->lon != $minPoint->lon) {
|
|
/* Get the line equation as y = mx + p */
|
|
$m = ($maxPoint->lat - $minPoint->lat) / ($maxPoint->lon - $minPoint->lon);
|
|
$p = ($maxPoint->lat * $minPoint->lon - $minPoint->lat * $maxPoint->lon) / ($minPoint->lon - $maxPoint->lon);
|
|
} else {
|
|
/* The line equation is y = p */
|
|
$m = null;
|
|
$p = $minPoint->lat;
|
|
$coef = (($minPoint->lon > $maxPoint->lon) ? 1 : -1);
|
|
}
|
|
|
|
/* For each point, check whether :
|
|
- it is on the right side of the line
|
|
- it is the farthest from the line
|
|
*/
|
|
foreach ($pointList as $point) {
|
|
if (isset($m)) {
|
|
$dist = $isBottom * ($m * $point->lon - $point->lat + $p);
|
|
} else {
|
|
$dist = $coef * ($point->lon - $p);
|
|
}
|
|
if ($dist > 0
|
|
&& $point->id != $minPoint->id
|
|
&& $point->id != $maxPoint->id) {
|
|
|
|
array_push($outsideList, $point);
|
|
$outsideListCount++;
|
|
|
|
if ($dist > $farthestDist) {
|
|
$farthestPoint = $point;
|
|
$farthestDist = $dist;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($outsideListCount == 0) {
|
|
return array($minPoint);
|
|
} else if ($outsideListCount == 1) {
|
|
return array($minPoint, $outsideList[0]);
|
|
} else {
|
|
return array_merge(
|
|
quickHullCalc($outsideList, $outsideListCount, $minPoint, $farthestPoint, $isBottom),
|
|
quickHullCalc($outsideList, $outsideListCount, $farthestPoint, $maxPoint, $isBottom));
|
|
}
|
|
}
|
|
|
|
/* This function receives a list of points [lat, lon] and returns a list of points [lat, lon]
|
|
representing the minimal convex polygon containing the points */
|
|
function quickHull(&$pointList, $count) {
|
|
|
|
$msg= 'Quick count='.$count;
|
|
|
|
if ($count == 0) {
|
|
return array();
|
|
} else if ($count == 1) {
|
|
return array($pointList[0]);
|
|
} else if ($count == 2) {
|
|
return array($pointList[0], $pointList[1]);
|
|
}
|
|
|
|
/* retrieves the min and max points on the x axe */
|
|
$minPoint = $pointList[0];
|
|
$maxPoint = $pointList[0];
|
|
|
|
foreach ($pointList as $point) {
|
|
if ($point->lon < $minPoint->lon || ($point->lon == $minPoint->lon && $point->lat < $minPoint->lat)) {
|
|
$minPoint = $point;
|
|
}
|
|
if ($point->lon > $maxPoint->lon || ($point->lon == $maxPoint->lon && $point->lat > $maxPoint->lat)) {
|
|
$maxPoint = $point;
|
|
}
|
|
}
|
|
|
|
$bottomPoints = quickHullCalc($pointList, $count, $minPoint, $maxPoint, 1);
|
|
$topPoints = quickHullCalc($pointList, $count, $maxPoint, $minPoint, -1);
|
|
return array_merge($bottomPoints, $topPoints);
|
|
}
|
|
|
|
|
|
|
|
|
|
$GRID_MAX_ZOOM = 16;
|
|
$GRID_CELL_PIXEL = 90;
|
|
|
|
/* Check if parameters are not empty */
|
|
if ( !array_key_exists('bbox', $_GET)
|
|
|| !array_key_exists('zoom', $_GET)
|
|
|| !array_key_exists('width', $_GET)
|
|
|| !array_key_exists('height', $_GET)) {
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"bbox, zoom, width and height parameters are required. '
|
|
.(array_key_exists('bbox', $_GET) ? '' : 'bbox is empty. ')
|
|
.(array_key_exists('zoom', $_GET) ? '' : 'zoom is empty. ')
|
|
.(array_key_exists('width', $_GET) ? '' : 'width is empty. ')
|
|
.(array_key_exists('height', $_GET) ? '' : 'height is empty. ')
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
/* Check zoom */
|
|
$zoom = $_GET['zoom'];
|
|
|
|
if ( !is_numeric($zoom)
|
|
|| intval($zoom) < 1
|
|
|| intval($zoom) > 19) {
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"unexpected zoom value : '
|
|
.htmlentities($zoom)
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
/* Check bounds box --> bbox=boundsSW.lng,boundsSW.lat,boundsNE.lng,boundsNE.lat */
|
|
$bbox = explode(',', $_GET['bbox']);
|
|
|
|
if ( !is_numeric($bbox[0]) || $bbox[0] > 180 // 0 = SW.lng
|
|
|| !is_numeric($bbox[2]) || $bbox[2] < -180 // 2 = NE.lng
|
|
|| !is_numeric($bbox[1]) || $bbox[1] < -90 || $bbox[1] > 90 // 1 = SW.lat
|
|
|| !is_numeric($bbox[3]) || $bbox[3] < -90 || $bbox[3] > 90) { // 3 = NE.lat
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"unexpected bbox longitude and latitude values : '
|
|
.'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).'], '
|
|
.'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']'
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
if ($bbox[0] >= $bbox[2]) { // 0 = SW.lng | 2 = NE.lng
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"min longitude greater than max longitude : '
|
|
.'lon ['.htmlentities($bbox[0]).', '.htmlentities($bbox[2]).']'
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
if ($bbox[1] >= $bbox[3]) { // 1 = SW.lat | 3 = NE.lat
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"min latitude greater than max latitude : '
|
|
.'lat ['.htmlentities($bbox[1]).', '.htmlentities($bbox[3]).']'
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
$lonMin = $bbox[0]; // 0 = SW.lng
|
|
$lonMax = $bbox[2]; // 2 = NE.lng
|
|
$latMin = $bbox[1]; // 1 = SW.lat
|
|
$latMax = $bbox[3]; // 3 = NE.lat
|
|
|
|
$pixelWidth = (int) $_GET['width'];
|
|
$pixelHeight = (int) $_GET['height'];
|
|
|
|
if ($pixelWidth == 0 || $pixelHeight == 0) {
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"width or height is null : '
|
|
.htmlentities($pixelWidth).'x'.htmlentities($pixelHeight)
|
|
.'"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
$lonWidth = $lonMax - $lonMin; // NE.lng - SW.lng
|
|
$latHeight = $latMax - $latMin; // NE.lat - SW.lat
|
|
|
|
$divWCount = ((int) ($pixelWidth / $GRID_CELL_PIXEL)) + 1;
|
|
$divHCount = ((int) ($pixelHeight / $GRID_CELL_PIXEL)) + 1;
|
|
|
|
$divWidth = $lonWidth / $divWCount;
|
|
$divHeight = $latHeight / $divHCount;
|
|
$divDiag2 = ($divWidth * $divWidth) + ($divHeight * $divHeight);
|
|
|
|
/* Connect to database */
|
|
$mysqli = new mysqli(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB);
|
|
if($mysqli->connect_errno) {
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"error while connecting to db : ' . $mysqli->error . '"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
$rqtLonMin = $lonMin; // SW.lng
|
|
$rqtLonMax = $lonMax; // NE.lng
|
|
|
|
/* Indicates whether the map displays the -180/180° longitude */
|
|
if ($rqtLonMax - $rqtLonMin > 360) {
|
|
$rqtLonMin = -180;
|
|
$rqtLonMax = 180;
|
|
}
|
|
|
|
/* Select the nodes to be returned, and cluster them on a grid if necessary */
|
|
if ($rqtLonMin >= -180 && $lonMax <= 180) {
|
|
$sql='SELECT id, latitude, longitude
|
|
FROM position
|
|
WHERE latitude between ? and ?
|
|
AND longitude between ? and ?';
|
|
} else {
|
|
$sql='SELECT id, latitude, longitude
|
|
FROM position
|
|
WHERE latitude between ? and ?
|
|
AND (longitude between ? and 1800000000 OR longitude between -1800000000 and ?)';
|
|
|
|
while ($rqtLonMin < -180) {
|
|
$rqtLonMin += 360;
|
|
}
|
|
while ($rqtLonMax > 180) {
|
|
$rqtLonMax -= 360;
|
|
}
|
|
}
|
|
|
|
$rqtLonMin = bcmul($rqtLonMin, 10000000, 0); // SW.lng
|
|
$rqtLonMax = bcmul($rqtLonMax, 10000000, 0); // NE.lng
|
|
$rqtLatMin = bcmul($latMin, 10000000, 0); // SW.lat
|
|
$rqtLatMax = bcmul($latMax, 10000000, 0); // NE.lat
|
|
|
|
$resArray = array();
|
|
$nbFetch = 0;
|
|
|
|
if ($stmt = $mysqli->prepare($sql)) {
|
|
$stmt->bind_param('iiii', $rqtLatMin, $rqtLatMax, $rqtLonMin, $rqtLonMax); // SW.lat, NE.lat, SW.lng, NE.lng
|
|
|
|
$stmt->execute();
|
|
|
|
$stmt->bind_result($id, $latitude, $longitude);
|
|
|
|
while ($stmt->fetch()) {
|
|
$nbFetch++;
|
|
|
|
$latitude = bcdiv($latitude, 10000000, 7);
|
|
$longitude = bcdiv($longitude, 10000000, 7);
|
|
|
|
while ($longitude < $lonMin) { // $lonMin = SW.lng
|
|
$longitude += 360;
|
|
}
|
|
while ($longitude > $lonMax) { // $lonMax = NE.lng
|
|
$longitude -= 360;
|
|
}
|
|
|
|
/* Initialize the current point */
|
|
if ($zoom >= $GRID_MAX_ZOOM) {
|
|
array_push($resArray, array(
|
|
'points' => array(new OsmPoint($id, $latitude, $longitude)),
|
|
'latitude' => $latitude,
|
|
'longitude' => $longitude,
|
|
'count' => 1 ));
|
|
} else {
|
|
$posLat = (int) (($latitude - $latMin) / $divHeight); // $latMin = SW.lat
|
|
$posLon = (int) (($longitude - $lonMin) / $divWidth); // $lonMin = SW.lng
|
|
$pos = $posLon.','.$posLat;
|
|
|
|
if (!array_key_exists($pos, $resArray)) {
|
|
$resArray[$pos] = array(
|
|
'points' => array(),
|
|
'latitude' => 0,
|
|
'longitude' => 0,
|
|
'count' => 0,
|
|
'grid' => $pos);
|
|
}
|
|
|
|
$elt = new OsmPoint($id, $latitude, $longitude);
|
|
array_push($resArray[$pos]['points'], $elt);
|
|
|
|
/* Increment the number of points */
|
|
$resArray[$pos]['count']++;
|
|
$resArray[$pos]['latitude'] += $latitude;
|
|
$resArray[$pos]['longitude'] += $longitude;
|
|
}
|
|
}
|
|
|
|
$stmt->close();
|
|
} else {
|
|
$mysqli->close();
|
|
header('Content-type: application/json');
|
|
$result = '{"error":"Error while request : ' . $mysqli->error . '"}';
|
|
echo $result;
|
|
exit;
|
|
}
|
|
|
|
/* Unify some clusters if nodes center are near */
|
|
$pointsCount = 0;
|
|
if ($zoom < $GRID_MAX_ZOOM) {
|
|
foreach($resArray as $val) {
|
|
mergeNeighborhood($resArray, $val['grid']);
|
|
$pointsCount += $val['count'];
|
|
}
|
|
}
|
|
|
|
$result='[';
|
|
$separator='';
|
|
|
|
/* Writing selected points */
|
|
$sql='SELECT k, v
|
|
FROM tag
|
|
WHERE id = ?
|
|
AND k NOT IN ("lat", "lon")';
|
|
|
|
$stmt = $mysqli->prepare($sql);
|
|
$stmt->bind_param('d', $id);
|
|
$stmt->bind_result($k, $v);
|
|
|
|
/* Writing grouped items or not */
|
|
foreach($resArray as $val) {
|
|
if ($val['count'] > 0) {
|
|
$result = $result.$separator
|
|
.'{"lat":"'.($val['latitude'] / $val['count']).'"'
|
|
.',"lon":"'.($val['longitude'] / $val['count']).'"';
|
|
|
|
if ($val['count'] == 1) {
|
|
$id = $val['points'][0]->id;
|
|
$result = $result.',"id":"'.$id.'"';
|
|
|
|
$stmt->execute();
|
|
|
|
while($stmt->fetch()) {
|
|
$result = $result.',"'.htmlentities($k).'":"'.htmlentities($v, ENT_COMPAT, 'UTF-8').'"';
|
|
}
|
|
} else {
|
|
$result = $result
|
|
.',"count":"'.$val['count'].'"'
|
|
.',"multi":"yes"'
|
|
.',"poly":[';
|
|
|
|
if ($pointsCount < MAX_POINTS_FOR_QUICKHULL) {
|
|
$convexPoly = quickHull($val['points'], $val['count']);
|
|
$sepPoly = '';
|
|
foreach($convexPoly as $point) {
|
|
$result = $result.$sepPoly.'{'
|
|
.'"lat":"'.$point->lat.'"'
|
|
.',"id":"'.$point->id.'"'
|
|
.',"lon":"'.$point->lon.'"}';
|
|
$sepPoly=',';
|
|
}
|
|
}
|
|
$result=$result.']';
|
|
}
|
|
|
|
$result = $result.'}';
|
|
$separator=',';
|
|
}
|
|
}
|
|
|
|
$stmt->close();
|
|
|
|
$result = $result . ']';
|
|
|
|
$mysqli->close();
|
|
|
|
header('Content-type: application/json; Charset : utf-8');
|
|
echo $result;
|
|
?>
|