258 lines
11 KiB
C#
258 lines
11 KiB
C#
using System.Globalization;
|
|
using System.IO.Compression;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Xml.Linq;
|
|
|
|
namespace LMEMappeCatastaliApp.Services;
|
|
|
|
public sealed class MapExportService
|
|
{
|
|
private static readonly XNamespace Kml = "http://www.opengis.net/kml/2.2";
|
|
|
|
public async Task<MapExportFile> CreateAsync(
|
|
string requestedName,
|
|
string requestedFormat,
|
|
string geoJson,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
using var document = JsonDocument.Parse(geoJson);
|
|
var root = document.RootElement;
|
|
if (!root.TryGetProperty("type", out var type) || type.GetString() != "FeatureCollection"
|
|
|| !root.TryGetProperty("features", out var features)
|
|
|| features.ValueKind != JsonValueKind.Array)
|
|
{
|
|
throw new InvalidDataException("I dati raccolti dalla mappa non sono validi.");
|
|
}
|
|
|
|
var featureCount = features.GetArrayLength();
|
|
if (featureCount == 0)
|
|
{
|
|
throw new InvalidOperationException("Nessun oggetto selezionato e disponibile da esportare.");
|
|
}
|
|
|
|
var format = requestedFormat.Trim().ToLowerInvariant() switch
|
|
{
|
|
"kml" => "kml",
|
|
"kmz" => "kmz",
|
|
_ => "geojson"
|
|
};
|
|
var baseName = NormalizeFileName(requestedName);
|
|
var exportDirectory = Path.Combine(FileSystem.CacheDirectory, "Exports");
|
|
Directory.CreateDirectory(exportDirectory);
|
|
var path = Path.Combine(exportDirectory, $"{baseName}.{format}");
|
|
|
|
if (format == "geojson")
|
|
{
|
|
using var output = File.Create(path);
|
|
await JsonSerializer.SerializeAsync(output, root, new JsonSerializerOptions { WriteIndented = true }, cancellationToken);
|
|
return new(path, "application/geo+json", featureCount, Path.GetFileName(path));
|
|
}
|
|
|
|
var kml = BuildKml(features, baseName);
|
|
if (format == "kml")
|
|
{
|
|
await File.WriteAllTextAsync(path, kml, new UTF8Encoding(false), cancellationToken);
|
|
return new(path, "application/vnd.google-earth.kml+xml", featureCount, Path.GetFileName(path));
|
|
}
|
|
|
|
await using (var output = File.Create(path))
|
|
using (var archive = new ZipArchive(output, ZipArchiveMode.Create))
|
|
{
|
|
var entry = archive.CreateEntry("doc.kml", CompressionLevel.Optimal);
|
|
await using var entryStream = entry.Open();
|
|
await using var writer = new StreamWriter(entryStream, new UTF8Encoding(false));
|
|
await writer.WriteAsync(kml.AsMemory(), cancellationToken);
|
|
}
|
|
return new(path, "application/vnd.google-earth.kmz", featureCount, Path.GetFileName(path));
|
|
}
|
|
|
|
private static string BuildKml(JsonElement features, string documentName)
|
|
{
|
|
var document = new XElement(Kml + "Document",
|
|
new XElement(Kml + "name", documentName),
|
|
BuildStyle("imported", "ff2ecc71", "402ecc71"),
|
|
BuildStyle("imported-sheet", "ff22803d", "1022803d"),
|
|
BuildStyle("cadastral", "ff4444ef", "4224bffb"),
|
|
BuildStyle("cadastral-gps", "ffff7a00", "337aff00"),
|
|
BuildStyle("drawing", "ff1679f9", "333c92fb"),
|
|
BuildStyle("poi", "ffff7a00", "33ff7a00"),
|
|
BuildStyle("gps-trail", "ff00a5ff", "2200a5ff"));
|
|
var categoryFolders = new Dictionary<string, XElement>(StringComparer.Ordinal);
|
|
var groupFolders = new Dictionary<(string Category, string Group), XElement>();
|
|
|
|
foreach (var feature in features.EnumerateArray())
|
|
{
|
|
if (!feature.TryGetProperty("geometry", out var geometry)
|
|
|| geometry.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var properties = feature.TryGetProperty("properties", out var propertyElement)
|
|
&& propertyElement.ValueKind == JsonValueKind.Object
|
|
? propertyElement
|
|
: default;
|
|
var category = GetString(properties, "_lmeCategory") ?? "drawing";
|
|
var placemark = new XElement(Kml + "Placemark",
|
|
new XElement(Kml + "name", FeatureName(properties, category)),
|
|
new XElement(Kml + "styleUrl", $"#{NormalizeStyleId(category)}"));
|
|
|
|
var extendedData = BuildExtendedData(properties);
|
|
if (extendedData is not null)
|
|
{
|
|
placemark.Add(extendedData);
|
|
}
|
|
placemark.Add(BuildGeometry(geometry));
|
|
var categoryName = GetString(properties, "_lmeCategoryName")?.Trim();
|
|
var groupName = GetString(properties, "_lmeGroupName")?.Trim();
|
|
var parent = document;
|
|
if (!string.IsNullOrWhiteSpace(categoryName))
|
|
{
|
|
if (!categoryFolders.TryGetValue(categoryName, out var categoryFolder))
|
|
{
|
|
categoryFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", categoryName));
|
|
categoryFolders[categoryName] = categoryFolder;
|
|
document.Add(categoryFolder);
|
|
}
|
|
parent = categoryFolder;
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(groupName))
|
|
{
|
|
var key = (categoryName ?? string.Empty, groupName);
|
|
if (!groupFolders.TryGetValue(key, out var groupFolder))
|
|
{
|
|
groupFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", groupName));
|
|
groupFolders[key] = groupFolder;
|
|
parent.Add(groupFolder);
|
|
}
|
|
parent = groupFolder;
|
|
}
|
|
parent.Add(placemark);
|
|
}
|
|
|
|
return new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement(Kml + "kml", document)).ToString();
|
|
}
|
|
|
|
private static XElement BuildStyle(string id, string lineColor, string fillColor) =>
|
|
new(Kml + "Style",
|
|
new XAttribute("id", id),
|
|
new XElement(Kml + "LineStyle",
|
|
new XElement(Kml + "color", lineColor),
|
|
new XElement(Kml + "width", 3)),
|
|
new XElement(Kml + "PolyStyle", new XElement(Kml + "color", fillColor)));
|
|
|
|
private static XElement? BuildExtendedData(JsonElement properties)
|
|
{
|
|
if (properties.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var data = properties.EnumerateObject()
|
|
.Where(item => item.Name != "_lmeCategory"
|
|
&& item.Value.ValueKind is JsonValueKind.String or JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
|
|
.Select(item => new XElement(Kml + "Data",
|
|
new XAttribute("name", item.Name),
|
|
new XElement(Kml + "value", ScalarText(item.Value))))
|
|
.ToArray();
|
|
return data.Length == 0 ? null : new XElement(Kml + "ExtendedData", data);
|
|
}
|
|
|
|
private static XElement BuildGeometry(JsonElement geometry)
|
|
{
|
|
var type = geometry.GetProperty("type").GetString();
|
|
if (type == "GeometryCollection")
|
|
{
|
|
return new XElement(Kml + "MultiGeometry",
|
|
geometry.GetProperty("geometries").EnumerateArray().Select(BuildGeometry));
|
|
}
|
|
|
|
var coordinates = geometry.GetProperty("coordinates");
|
|
return type switch
|
|
{
|
|
"Point" => new XElement(Kml + "Point", CoordinatesElement([coordinates])),
|
|
"LineString" => new XElement(Kml + "LineString", CoordinatesElement(coordinates.EnumerateArray())),
|
|
"Polygon" => BuildPolygon(coordinates),
|
|
"MultiPoint" => new XElement(Kml + "MultiGeometry",
|
|
coordinates.EnumerateArray().Select(point => new XElement(Kml + "Point", CoordinatesElement([point])))),
|
|
"MultiLineString" => new XElement(Kml + "MultiGeometry",
|
|
coordinates.EnumerateArray().Select(line => new XElement(Kml + "LineString", CoordinatesElement(line.EnumerateArray())))),
|
|
"MultiPolygon" => new XElement(Kml + "MultiGeometry", coordinates.EnumerateArray().Select(BuildPolygon)),
|
|
_ => throw new InvalidDataException($"Geometria GeoJSON non supportata: {type}.")
|
|
};
|
|
}
|
|
|
|
private static XElement BuildPolygon(JsonElement rings)
|
|
{
|
|
var ringList = rings.EnumerateArray().ToArray();
|
|
var polygon = new XElement(Kml + "Polygon");
|
|
if (ringList.Length > 0)
|
|
{
|
|
polygon.Add(new XElement(Kml + "outerBoundaryIs",
|
|
new XElement(Kml + "LinearRing", CoordinatesElement(ringList[0].EnumerateArray()))));
|
|
}
|
|
foreach (var ring in ringList.Skip(1))
|
|
{
|
|
polygon.Add(new XElement(Kml + "innerBoundaryIs",
|
|
new XElement(Kml + "LinearRing", CoordinatesElement(ring.EnumerateArray()))));
|
|
}
|
|
return polygon;
|
|
}
|
|
|
|
private static XElement CoordinatesElement(IEnumerable<JsonElement> points) =>
|
|
new(Kml + "coordinates", string.Join(" ", points.Select(FormatCoordinate)));
|
|
|
|
private static string FormatCoordinate(JsonElement coordinate)
|
|
{
|
|
var values = coordinate.EnumerateArray().Take(3)
|
|
.Select(value => value.GetDouble().ToString("G17", CultureInfo.InvariantCulture));
|
|
return string.Join(",", values);
|
|
}
|
|
|
|
private static string FeatureName(JsonElement properties, string category) =>
|
|
GetString(properties, "name")
|
|
?? GetString(properties, "Name")
|
|
?? GetString(properties, "LABEL")
|
|
?? GetString(properties, "NATIONALCADASTRALREFERENCE")
|
|
?? category switch
|
|
{
|
|
"imported-sheet" => "Cornice o foglio importato",
|
|
"cadastral" => "Particella catastale",
|
|
"cadastral-gps" => "Particella GPS",
|
|
"poi" => "Punto di interesse",
|
|
"gps-trail" => "Traccia GPS",
|
|
_ => "Oggetto mappa"
|
|
};
|
|
|
|
private static string? GetString(JsonElement element, string name) =>
|
|
element.ValueKind == JsonValueKind.Object
|
|
&& element.TryGetProperty(name, out var value)
|
|
&& value.ValueKind == JsonValueKind.String
|
|
? value.GetString()
|
|
: null;
|
|
|
|
private static string ScalarText(JsonElement value) => value.ValueKind == JsonValueKind.String
|
|
? value.GetString() ?? string.Empty
|
|
: value.GetRawText();
|
|
|
|
private static string NormalizeStyleId(string category) => category switch
|
|
{
|
|
"imported" or "imported-sheet" or "cadastral" or "cadastral-gps" or "drawing" or "poi" or "gps-trail" => category,
|
|
_ => "drawing"
|
|
};
|
|
|
|
private static string NormalizeFileName(string requestedName)
|
|
{
|
|
var name = string.IsNullOrWhiteSpace(requestedName) ? "Mappa" : requestedName.Trim();
|
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
|
{
|
|
name = name.Replace(invalid, '_');
|
|
}
|
|
name = Path.GetFileNameWithoutExtension(name).Trim().TrimEnd('.');
|
|
return string.IsNullOrWhiteSpace(name) ? "Mappa" : name[..Math.Min(name.Length, 80)];
|
|
}
|
|
}
|
|
|
|
public sealed record MapExportFile(string Path, string MimeType, int FeatureCount, string FileName);
|