272 lines
7.8 KiB
PHP
272 lines
7.8 KiB
PHP
<?php
|
|
/**
|
|
* Polyline Utility
|
|
* Encoding/Decoding Google Maps Polyline Algorithm Format
|
|
* Used for efficient route storage and transmission
|
|
*/
|
|
|
|
class PolylineUtility {
|
|
/**
|
|
* Decode polyline string to array of coordinates
|
|
* Implementation of Google's Encoded Polyline Algorithm Format
|
|
*
|
|
* @param string $encoded The encoded polyline string
|
|
* @return array Array of coordinates with 'lat' and 'lng' keys
|
|
*/
|
|
public static function decodePolyline($encoded) {
|
|
$inv = 1.0 / 1e5;
|
|
$decoded = [];
|
|
$previous = [0, 0];
|
|
$i = 0;
|
|
|
|
while ($i < strlen($encoded)) {
|
|
$ll = [0, 0];
|
|
|
|
for ($j = 0; $j < 2; $j++) {
|
|
$shift = 0;
|
|
$result = 0;
|
|
|
|
do {
|
|
$byte = ord(substr($encoded, $i++, 1)) - 63;
|
|
$result |= ($byte & 0x1f) << $shift;
|
|
$shift += 5;
|
|
} while ($byte >= 0x20 && $i < strlen($encoded));
|
|
|
|
$dlng = ($result & 1) ? ~($result >> 1) : ($result >> 1);
|
|
$ll[$j] = $previous[$j] + $dlng;
|
|
$previous[$j] = $ll[$j];
|
|
}
|
|
|
|
$decoded[] = [
|
|
'lat' => $ll[0] * $inv,
|
|
'lng' => $ll[1] * $inv
|
|
];
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
/**
|
|
* Encode array of coordinates to polyline string
|
|
* Implementation of Google's Encoded Polyline Algorithm Format
|
|
*
|
|
* @param array $coordinates Array of coordinates with 'lat' and 'lng' keys
|
|
* @return string The encoded polyline string
|
|
*/
|
|
public static function encodePolyline($coordinates) {
|
|
$encoded = '';
|
|
$previous = [0, 0];
|
|
|
|
foreach ($coordinates as $point) {
|
|
$lat = $point['lat'];
|
|
$lng = $point['lng'];
|
|
|
|
$current = [
|
|
intval(round($lat * 1e5)),
|
|
intval(round($lng * 1e5))
|
|
];
|
|
|
|
for ($j = 0; $j < 2; $j++) {
|
|
$curr = $current[$j];
|
|
$prev = $previous[$j];
|
|
$value = $curr - $prev;
|
|
|
|
$value = ($value << 1) ^ ($value >> 31);
|
|
$chunks = [];
|
|
|
|
while ($value >= 0x20) {
|
|
$chunks[] = (0x20 | ($value & 0x1f)) + 63;
|
|
$value >>= 5;
|
|
}
|
|
|
|
$chunks[] = $value + 63;
|
|
|
|
foreach ($chunks as $chunk) {
|
|
$encoded .= chr($chunk);
|
|
}
|
|
|
|
$previous[$j] = $current[$j];
|
|
}
|
|
}
|
|
|
|
return $encoded;
|
|
}
|
|
|
|
/**
|
|
* Calculate distance between two coordinates using Haversine formula
|
|
* Returns distance in meters
|
|
*
|
|
* @param float $lat1 Starting latitude
|
|
* @param float $lng1 Starting longitude
|
|
* @param float $lat2 Ending latitude
|
|
* @param float $lng2 Ending longitude
|
|
* @return float Distance in meters
|
|
*/
|
|
public static function calculateDistance($lat1, $lng1, $lat2, $lng2) {
|
|
$earth_radius = 6371000; // Earth's radius in meters
|
|
|
|
$dLat = deg2rad($lat2 - $lat1);
|
|
$dLng = deg2rad($lng2 - $lng1);
|
|
|
|
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
|
sin($dLng / 2) * sin($dLng / 2);
|
|
|
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
$distance = $earth_radius * $c;
|
|
|
|
return round($distance, 2);
|
|
}
|
|
|
|
/**
|
|
* Calculate total distance of a route from coordinates array
|
|
*
|
|
* @param array $coordinates Array of coordinates with 'lat' and 'lng' keys
|
|
* @return float Total distance in meters
|
|
*/
|
|
public static function calculateTotalDistance($coordinates) {
|
|
if (count($coordinates) < 2) {
|
|
return 0;
|
|
}
|
|
|
|
$total_distance = 0;
|
|
|
|
for ($i = 0; $i < count($coordinates) - 1; $i++) {
|
|
$current = $coordinates[$i];
|
|
$next = $coordinates[$i + 1];
|
|
|
|
$segment_distance = self::calculateDistance(
|
|
$current['lat'],
|
|
$current['lng'],
|
|
$next['lat'],
|
|
$next['lng']
|
|
);
|
|
|
|
$total_distance += $segment_distance;
|
|
}
|
|
|
|
return round($total_distance, 2);
|
|
}
|
|
|
|
/**
|
|
* Calculate elevation gain and loss from coordinates with elevation data
|
|
*
|
|
* @param array $coordinates Array of coordinates with 'lat', 'lng', and 'elevation' keys
|
|
* @return array ['gain' => float, 'loss' => float]
|
|
*/
|
|
public static function calculateElevationChange($coordinates) {
|
|
$elevation_gain = 0;
|
|
$elevation_loss = 0;
|
|
|
|
for ($i = 0; $i < count($coordinates) - 1; $i++) {
|
|
$current_elevation = $coordinates[$i]['elevation'] ?? null;
|
|
$next_elevation = $coordinates[$i + 1]['elevation'] ?? null;
|
|
|
|
if ($current_elevation !== null && $next_elevation !== null) {
|
|
$change = $next_elevation - $current_elevation;
|
|
|
|
if ($change > 0) {
|
|
$elevation_gain += $change;
|
|
} else {
|
|
$elevation_loss += abs($change);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'gain' => round($elevation_gain, 2),
|
|
'loss' => round($elevation_loss, 2)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Simplify polyline using Douglas-Peucker algorithm
|
|
* Reduces number of points while maintaining shape accuracy
|
|
*
|
|
* @param array $coordinates Array of coordinates
|
|
* @param float $tolerance Distance tolerance in degrees
|
|
* @return array Simplified coordinates array
|
|
*/
|
|
public static function simplifyPolyline($coordinates, $tolerance = 0.00001) {
|
|
if (count($coordinates) < 3) {
|
|
return $coordinates;
|
|
}
|
|
|
|
$dmax = 0;
|
|
$index = 0;
|
|
|
|
for ($i = 1; $i < count($coordinates) - 1; $i++) {
|
|
$d = self::pointLineDistance(
|
|
$coordinates[$i],
|
|
$coordinates[0],
|
|
$coordinates[count($coordinates) - 1]
|
|
);
|
|
|
|
if ($d > $dmax) {
|
|
$dmax = $d;
|
|
$index = $i;
|
|
}
|
|
}
|
|
|
|
if ($dmax > $tolerance) {
|
|
$rec1 = self::simplifyPolyline(
|
|
array_slice($coordinates, 0, $index + 1),
|
|
$tolerance
|
|
);
|
|
$rec2 = self::simplifyPolyline(
|
|
array_slice($coordinates, $index),
|
|
$tolerance
|
|
);
|
|
|
|
$result = array_merge(array_slice($rec1, 0, -1), $rec2);
|
|
} else {
|
|
$result = [
|
|
$coordinates[0],
|
|
$coordinates[count($coordinates) - 1]
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Calculate perpendicular distance from point to line
|
|
*/
|
|
private static function pointLineDistance($point, $lineStart, $lineEnd) {
|
|
$px = $point['lat'];
|
|
$py = $point['lng'];
|
|
$x1 = $lineStart['lat'];
|
|
$y1 = $lineStart['lng'];
|
|
$x2 = $lineEnd['lat'];
|
|
$y2 = $lineEnd['lng'];
|
|
|
|
$numerator = abs(($y2 - $y1) * $px - ($x2 - $x1) * $py + $x2 * $y1 - $y2 * $x1);
|
|
$denominator = sqrt(pow($y2 - $y1, 2) + pow($x2 - $x1, 2));
|
|
|
|
return $denominator != 0 ? $numerator / $denominator : 0;
|
|
}
|
|
|
|
/**
|
|
* Validate polyline string format
|
|
*
|
|
* @param string $polyline The encoded polyline
|
|
* @return bool True if valid polyline format
|
|
*/
|
|
public static function validatePolyline($polyline) {
|
|
if (!is_string($polyline) || empty($polyline)) {
|
|
return false;
|
|
}
|
|
|
|
// Check that all characters are valid ASCII printable
|
|
for ($i = 0; $i < strlen($polyline); $i++) {
|
|
$char_code = ord($polyline[$i]);
|
|
if ($char_code < 63 || $char_code > 126) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
?>
|