mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 21:41:14 +00:00
[AZ-367] Refactor C14: extract shared TileGridStitcher
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace SatelliteProvider.Common.Imaging;
|
||||
|
||||
// AZ-367 / C14: shared tile-grid stitcher.
|
||||
// Both region and route map generation previously implemented the same placement
|
||||
// loop (min/max of TileX/TileY, allocate Image<Rgb24>, foreach tile load + DrawImage
|
||||
// at (x-minX, y-minY) * tileSizePixels). The overlays (region center cross / route
|
||||
// geofence rectangles + route point crosses) differ and stay at the call sites.
|
||||
public sealed class TileGridStitcher
|
||||
{
|
||||
public async Task<StitchResult> StitchAsync(
|
||||
IEnumerable<TilePlacement> tiles,
|
||||
int tileSizePixels,
|
||||
bool deduplicateByTileCoords,
|
||||
bool swallowTileLoadErrors,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tiles);
|
||||
if (tileSizePixels <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(tileSizePixels), tileSizePixels, "Tile size must be positive.");
|
||||
}
|
||||
|
||||
var sourceList = tiles.ToList();
|
||||
if (sourceList.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No tiles to stitch.");
|
||||
}
|
||||
|
||||
var orderedTiles = deduplicateByTileCoords
|
||||
? sourceList
|
||||
.GroupBy(t => (t.TileX, t.TileY))
|
||||
.Select(g => g.First())
|
||||
.OrderBy(t => t.TileY)
|
||||
.ThenBy(t => t.TileX)
|
||||
.ToList()
|
||||
: sourceList;
|
||||
|
||||
var minX = orderedTiles.Min(t => t.TileX);
|
||||
var maxX = orderedTiles.Max(t => t.TileX);
|
||||
var minY = orderedTiles.Min(t => t.TileY);
|
||||
var maxY = orderedTiles.Max(t => t.TileY);
|
||||
|
||||
var gridWidth = maxX - minX + 1;
|
||||
var gridHeight = maxY - minY + 1;
|
||||
var imageWidth = gridWidth * tileSizePixels;
|
||||
var imageHeight = gridHeight * tileSizePixels;
|
||||
|
||||
var stitchedImage = new Image<Rgb24>(imageWidth, imageHeight);
|
||||
var placed = new List<TilePlacement>(orderedTiles.Count);
|
||||
var missing = new List<MissingTile>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var tile in orderedTiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!File.Exists(tile.FilePath))
|
||||
{
|
||||
missing.Add(new MissingTile(tile, MissingTileReason.FileNotFound, Error: null));
|
||||
continue;
|
||||
}
|
||||
|
||||
Image<Rgb24>? tileImage = null;
|
||||
try
|
||||
{
|
||||
tileImage = await Image.LoadAsync<Rgb24>(tile.FilePath, cancellationToken);
|
||||
var destX = (tile.TileX - minX) * tileSizePixels;
|
||||
var destY = (tile.TileY - minY) * tileSizePixels;
|
||||
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
|
||||
placed.Add(tile);
|
||||
}
|
||||
catch (Exception ex) when (swallowTileLoadErrors && ex is not OperationCanceledException)
|
||||
{
|
||||
missing.Add(new MissingTile(tile, MissingTileReason.LoadFailed, ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
tileImage?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
stitchedImage.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new StitchResult(
|
||||
Image: stitchedImage,
|
||||
MinX: minX,
|
||||
MinY: minY,
|
||||
MaxX: maxX,
|
||||
MaxY: maxY,
|
||||
TileSizePixels: tileSizePixels,
|
||||
ImageWidth: imageWidth,
|
||||
ImageHeight: imageHeight,
|
||||
PlacedTiles: placed,
|
||||
MissingTiles: missing);
|
||||
}
|
||||
|
||||
public void DrawCross(Image<Rgb24> image, Point center, Rgb24 color, int armLength, int thickness = 10)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
if (armLength < 0 || thickness < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var halfThickness = thickness / 2;
|
||||
var width = image.Width;
|
||||
var height = image.Height;
|
||||
|
||||
for (int dx = -armLength; dx <= armLength; dx++)
|
||||
{
|
||||
for (int t = -halfThickness; t <= halfThickness; t++)
|
||||
{
|
||||
int x = center.X + dx;
|
||||
int y = center.Y + t;
|
||||
if (x >= 0 && x < width && y >= 0 && y < height)
|
||||
{
|
||||
image[x, y] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int dy = -armLength; dy <= armLength; dy++)
|
||||
{
|
||||
for (int t = -halfThickness; t <= halfThickness; t++)
|
||||
{
|
||||
int x = center.X + t;
|
||||
int y = center.Y + dy;
|
||||
if (x >= 0 && x < width && y >= 0 && y < height)
|
||||
{
|
||||
image[x, y] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawRectangleBorder(Image<Rgb24> image, Rectangle rect, Rgb24 color, int thickness = 5)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
if (thickness < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var x1 = rect.X;
|
||||
var y1 = rect.Y;
|
||||
var x2 = rect.X + rect.Width - 1;
|
||||
var y2 = rect.Y + rect.Height - 1;
|
||||
var width = image.Width;
|
||||
var height = image.Height;
|
||||
|
||||
for (int t = 0; t < thickness; t++)
|
||||
{
|
||||
for (int x = x1; x <= x2; x++)
|
||||
{
|
||||
int topY = y1 + t;
|
||||
int bottomY = y2 - t;
|
||||
if (x >= 0 && x < width && topY >= 0 && topY < height)
|
||||
{
|
||||
image[x, topY] = color;
|
||||
}
|
||||
if (x >= 0 && x < width && bottomY >= 0 && bottomY < height)
|
||||
{
|
||||
image[x, bottomY] = color;
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = y1; y <= y2; y++)
|
||||
{
|
||||
int leftX = x1 + t;
|
||||
int rightX = x2 - t;
|
||||
if (leftX >= 0 && leftX < width && y >= 0 && y < height)
|
||||
{
|
||||
image[leftX, y] = color;
|
||||
}
|
||||
if (rightX >= 0 && rightX < width && y >= 0 && y < height)
|
||||
{
|
||||
image[rightX, y] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TilePlacement(int TileX, int TileY, string FilePath);
|
||||
|
||||
public sealed record StitchResult(
|
||||
Image<Rgb24> Image,
|
||||
int MinX,
|
||||
int MinY,
|
||||
int MaxX,
|
||||
int MaxY,
|
||||
int TileSizePixels,
|
||||
int ImageWidth,
|
||||
int ImageHeight,
|
||||
IReadOnlyList<TilePlacement> PlacedTiles,
|
||||
IReadOnlyList<MissingTile> MissingTiles);
|
||||
|
||||
public sealed record MissingTile(TilePlacement Tile, MissingTileReason Reason, Exception? Error);
|
||||
|
||||
public enum MissingTileReason
|
||||
{
|
||||
FileNotFound,
|
||||
LoadFailed
|
||||
}
|
||||
Reference in New Issue
Block a user