Files
satellite-provider/SatelliteProvider.Common/Utils/GeoUtils.cs
T
Oleksandr Bezdieniezhnykh 6099d1c86b
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-376] [AZ-378] [AZ-379] [AZ-380] Repo cleanup: dead code, logger discipline, ColumnList consts
Batch 23 of refactor 03-code-quality-refactoring (4 tasks, 5 SP):

- AZ-376 (C23): Delete unused FindExistingTileAsync from
  ITileRepository / TileRepository. No callers; method also took the
  obsolete `version` arg removed by C06/AZ-357.
- AZ-378 (C25): Repository _logger discipline.
  TileRepository.GetTilesByRegionAsync now emits LogWarning when the
  query exceeds SlowQueryThresholdMs (500 ms). RegionRepository and
  RouteRepository drop the unused ILogger<TRepo> field, parameter, and
  using; Program.cs DI registrations updated.
- AZ-379 (C26): Extract `private const string ColumnList` per repo
  (TileRepository, RegionRepository, RouteRepository); SELECTs use
  $@"SELECT {ColumnList} FROM ..." (C# 10+ const interpolation).
  INSERT/UPDATE/DELETE unchanged; route_points single-site SELECT left
  inline.
- AZ-380 (C27): Delete dead alias GeoUtils.CalculatePolygonDiagonalDistance.

Tests: +9 new (RepositoryRefactorTests x8, GeoUtilsRefactorTests x1)
covering each AC via reflection / file-content assertions; pattern
mirrors ToolingConfigurationTests (b22) and AcceptanceCriteriaRT2Tests
(b19). Unit suite 181 -> 190, all green. dotnet format clean.

Code review: PASS_WITH_WARNINGS (3 Low findings, all informational or
out-of-scope for this batch). See
_docs/03_implementation/reviews/batch_23_review.md.

Cumulative review counter 2/3; next K=3 review fires after batch 24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:57:49 +03:00

129 lines
4.7 KiB
C#

using SatelliteProvider.Common.DTO;
namespace SatelliteProvider.Common.Utils;
public static class GeoUtils
{
private const double EARTH_RADIUS = 6378137;
public static (int x, int y) WorldToTilePos(GeoPoint point, int zoom)
{
var latRad = point.Lat * Math.PI / 180.0;
var n = Math.Pow(2.0, zoom);
var xTile = (int)Math.Floor((point.Lon + 180.0) / 360.0 * n);
var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
return (xTile, yTile);
}
public static double ToRadians(double degrees) => degrees * Math.PI / 180.0;
public static double ToDegrees(double radians) => radians * 180.0 / Math.PI;
public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2)
{
var lat1Rad = ToRadians(p1.Lat);
var lat2Rad = ToRadians(p2.Lat);
var dLon = ToRadians(p2.Lon - p1.Lon);
var dLat = ToRadians(p2.Lat - p1.Lat);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Asin(Math.Sqrt(a));
var distance = EARTH_RADIUS * c;
var y = Math.Sin(dLon) * Math.Cos(lat2Rad);
var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon);
var azimuthRadians = Math.Atan2(y, x);
var azimuth = (ToDegrees(azimuthRadians) + 360) % 360;
return new Direction
{
Distance = distance,
Azimuth = azimuth
};
}
public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction)
{
var angularDistance = direction.Distance / EARTH_RADIUS;
var azimuthRadians = ToRadians(direction.Azimuth);
var startLatRad = ToRadians(startPoint.Lat);
var startLonRad = ToRadians(startPoint.Lon);
var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) +
Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians));
var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad),
Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad));
return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad));
}
public static GeoPoint TileToWorldPos(int x, int y, int zoom)
{
var n = Math.Pow(2.0, zoom);
var lonDeg = x / n * 360.0 - 180.0;
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
var latDeg = latRad * 180.0 / Math.PI;
return new GeoPoint(latDeg, lonDeg);
}
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
{
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
var minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0);
var maxLat = Math.Min(centerGeoPoint.Lat + latDiff, 90.0);
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
return (minLat, maxLat, minLon, maxLon);
}
public static List<GeoPoint> CalculateIntermediatePoints(GeoPoint start, GeoPoint end, double maxSpacingMeters)
{
var direction = start.DirectionTo(end);
var distance = direction.Distance;
if (distance <= maxSpacingMeters)
{
return new List<GeoPoint>();
}
var numSegments = (int)Math.Ceiling(distance / maxSpacingMeters);
var actualSpacing = distance / numSegments;
var intermediatePoints = new List<GeoPoint>();
for (int i = 1; i < numSegments; i++)
{
var segmentDistance = actualSpacing * i;
var intermediateDirection = new Direction
{
Distance = segmentDistance,
Azimuth = direction.Azimuth
};
var intermediatePoint = start.GoDirection(intermediateDirection);
intermediatePoints.Add(intermediatePoint);
}
return intermediatePoints;
}
public static double CalculateDistance(GeoPoint p1, GeoPoint p2)
{
return p1.DirectionTo(p2).Distance;
}
public static GeoPoint CalculateCenter(GeoPoint northWest, GeoPoint southEast)
{
var centerLat = (northWest.Lat + southEast.Lat) / 2.0;
var centerLon = (northWest.Lon + southEast.Lon) / 2.0;
return new GeoPoint(centerLat, centerLon);
}
}