Files
fitness/backend/PolylineUtility.php
2026-10-04 00:19:45 +03:00

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;
}
}
?>