2025-03-31 21:33:52 +02:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using System;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.IO.Compression;
|
|
|
|
|
|
using System.Linq;
|
2025-09-09 07:13:50 +02:00
|
|
|
|
using System.Threading;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
using System.Threading.Tasks;
|
2024-06-22 08:10:45 +02:00
|
|
|
|
using System.Xml.Linq;
|
2024-05-22 11:25:32 +02:00
|
|
|
|
#if WPF
|
|
|
|
|
|
using System.Windows;
|
|
|
|
|
|
using System.Windows.Controls;
|
|
|
|
|
|
using System.Windows.Media;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
#elif UWP
|
|
|
|
|
|
using Windows.UI.Xaml;
|
|
|
|
|
|
using Windows.UI.Xaml.Controls;
|
|
|
|
|
|
using Windows.UI.Xaml.Media;
|
2024-05-22 11:25:32 +02:00
|
|
|
|
#elif WINUI
|
|
|
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
|
|
|
|
|
using Microsoft.UI.Xaml.Media;
|
2025-08-19 19:43:02 +02:00
|
|
|
|
#elif AVALONIA
|
|
|
|
|
|
using Avalonia.Controls;
|
|
|
|
|
|
using Avalonia.Media;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
namespace MapControl
|
|
|
|
|
|
{
|
2022-01-19 16:43:00 +01:00
|
|
|
|
public class GroundOverlay : MapPanel
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2024-09-13 23:50:30 +02:00
|
|
|
|
private class ImageOverlay
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
public ImageOverlay(string path, LatLonBox latLonBox, int zIndex)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
ImagePath = path;
|
|
|
|
|
|
SetBoundingBox(Image, latLonBox);
|
|
|
|
|
|
Image.SetValue(Canvas.ZIndexProperty, zIndex);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 08:51:48 +02:00
|
|
|
|
public string ImagePath { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public Image Image { get; } = new Image { Stretch = Stretch.Fill };
|
|
|
|
|
|
|
|
|
|
|
|
public async Task LoadImage(Uri docUri)
|
|
|
|
|
|
{
|
|
|
|
|
|
Image.Source = await ImageLoader.LoadImageAsync(new Uri(docUri, ImagePath));
|
|
|
|
|
|
}
|
2025-09-04 16:29:26 +02:00
|
|
|
|
|
|
|
|
|
|
public async Task LoadImage(ZipArchive archive)
|
2025-09-04 08:11:15 +02:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
var entry = archive.GetEntry(ImagePath);
|
2025-09-04 08:11:15 +02:00
|
|
|
|
|
|
|
|
|
|
if (entry != null)
|
|
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
using (var memoryStream = new MemoryStream((int)entry.Length))
|
2025-09-04 08:11:15 +02:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
using (var zipStream = entry.Open())
|
|
|
|
|
|
{
|
|
|
|
|
|
zipStream.CopyTo(memoryStream); // can't use CopyToAsync with ZipArchive
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 10:36:39 +02:00
|
|
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
2025-09-11 16:46:42 +02:00
|
|
|
|
|
2025-09-05 08:51:48 +02:00
|
|
|
|
Image.Source = await ImageLoader.LoadImageAsync(memoryStream);
|
2025-09-04 08:11:15 +02:00
|
|
|
|
}
|
2025-09-04 10:36:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 11:06:37 +02:00
|
|
|
|
private static ILogger logger;
|
|
|
|
|
|
private static ILogger Logger => logger ?? (logger = ImageLoader.LoggerFactory?.CreateLogger<GroundOverlay>());
|
|
|
|
|
|
|
2025-09-08 23:09:14 +02:00
|
|
|
|
public static int MaxLoadTasks { get; set; } = 4;
|
|
|
|
|
|
|
2024-05-22 17:04:31 +02:00
|
|
|
|
public static readonly DependencyProperty SourcePathProperty =
|
2024-05-23 18:22:52 +02:00
|
|
|
|
DependencyPropertyHelper.Register<GroundOverlay, string>(nameof(SourcePath), null,
|
2024-09-13 23:47:17 +02:00
|
|
|
|
async (groundOverlay, oldValue, newValue) => await groundOverlay.LoadAsync(newValue));
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2022-01-18 23:44:08 +01:00
|
|
|
|
public string SourcePath
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2022-08-06 10:40:59 +02:00
|
|
|
|
get => (string)GetValue(SourcePathProperty);
|
|
|
|
|
|
set => SetValue(SourcePathProperty, value);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-13 23:47:17 +02:00
|
|
|
|
public static async Task<GroundOverlay> CreateAsync(string sourcePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var groundOverlay = new GroundOverlay();
|
|
|
|
|
|
|
|
|
|
|
|
await groundOverlay.LoadAsync(sourcePath);
|
|
|
|
|
|
|
|
|
|
|
|
return groundOverlay;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task LoadAsync(string sourcePath)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-04 16:29:26 +02:00
|
|
|
|
List<ImageOverlay> imageOverlays = null;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2022-01-18 23:44:08 +01:00
|
|
|
|
if (!string.IsNullOrEmpty(sourcePath))
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2022-01-18 23:44:08 +01:00
|
|
|
|
var ext = Path.GetExtension(sourcePath).ToLower();
|
|
|
|
|
|
|
2022-01-18 21:30:22 +01:00
|
|
|
|
if (ext == ".kmz")
|
|
|
|
|
|
{
|
2025-09-08 23:09:14 +02:00
|
|
|
|
imageOverlays = await LoadImageOverlaysFromArchive(sourcePath);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
else if (ext == ".kml")
|
|
|
|
|
|
{
|
2025-09-08 23:09:14 +02:00
|
|
|
|
imageOverlays = await LoadImageOverlaysFromFile(sourcePath);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-08-22 11:06:37 +02:00
|
|
|
|
Logger?.LogError(ex, "Failed loading from {path}", sourcePath);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-05-22 17:04:31 +02:00
|
|
|
|
Children.Clear();
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
|
|
|
|
|
if (imageOverlays != null)
|
|
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
foreach (var imageOverlay in imageOverlays)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
Children.Add(imageOverlay.Image);
|
2024-09-06 16:03:16 +02:00
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 23:09:14 +02:00
|
|
|
|
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromArchive(string archiveFilePath)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2023-01-11 17:51:00 +01:00
|
|
|
|
using (var archive = ZipFile.OpenRead(archiveFilePath))
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2023-01-11 17:51:00 +01:00
|
|
|
|
var docEntry = archive.GetEntry("doc.kml") ??
|
2025-09-01 18:21:41 +02:00
|
|
|
|
archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml")) ??
|
|
|
|
|
|
throw new ArgumentException($"No KML entry found in {archiveFilePath}.");
|
2025-09-09 08:18:25 +02:00
|
|
|
|
XDocument document;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2025-09-01 18:21:41 +02:00
|
|
|
|
using (var docStream = docEntry.Open())
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-09 08:18:25 +02:00
|
|
|
|
document = await LoadXDocument(docStream);
|
2025-09-01 18:21:41 +02:00
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2025-09-09 08:18:25 +02:00
|
|
|
|
return await LoadImageOverlays(document, imageOverlay => imageOverlay.LoadImage(archive));
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 23:09:14 +02:00
|
|
|
|
private static async Task<List<ImageOverlay>> LoadImageOverlaysFromFile(string docFilePath)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-04 16:29:26 +02:00
|
|
|
|
var docUri = new Uri(FilePath.GetFullPath(docFilePath));
|
2025-09-09 08:18:25 +02:00
|
|
|
|
XDocument document;
|
2025-09-04 16:29:26 +02:00
|
|
|
|
|
|
|
|
|
|
using (var docStream = File.OpenRead(docUri.AbsolutePath))
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-09 08:18:25 +02:00
|
|
|
|
document = await LoadXDocument(docStream);
|
2025-09-01 18:21:41 +02:00
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2025-09-09 08:18:25 +02:00
|
|
|
|
return await LoadImageOverlays(document, imageOverlay => imageOverlay.LoadImage(docUri));
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 08:18:25 +02:00
|
|
|
|
private static async Task<List<ImageOverlay>> LoadImageOverlays(XDocument document, Func<ImageOverlay, Task> loadFunc)
|
2025-09-08 23:09:14 +02:00
|
|
|
|
{
|
2025-09-09 08:18:25 +02:00
|
|
|
|
var imageOverlays = ReadImageOverlays(document);
|
|
|
|
|
|
|
2025-09-09 13:47:10 +02:00
|
|
|
|
using (var semaphore = new SemaphoreSlim(MaxLoadTasks))
|
2025-09-08 23:09:14 +02:00
|
|
|
|
{
|
2025-09-09 13:47:10 +02:00
|
|
|
|
var tasks = imageOverlays.Select(
|
|
|
|
|
|
async imageOverlay =>
|
|
|
|
|
|
{
|
2025-09-09 17:54:43 +02:00
|
|
|
|
// Limit number of simultaneous calls of loadFunc (in UI thread).
|
|
|
|
|
|
//
|
2025-09-09 13:47:10 +02:00
|
|
|
|
await semaphore.WaitAsync();
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-09-09 17:54:43 +02:00
|
|
|
|
await loadFunc(imageOverlay);
|
2025-09-09 13:47:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
semaphore.Release();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-09-08 23:09:14 +02:00
|
|
|
|
|
2025-09-09 13:47:10 +02:00
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
|
|
}
|
2025-09-09 08:18:25 +02:00
|
|
|
|
|
|
|
|
|
|
return imageOverlays;
|
2025-09-08 23:09:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 08:18:25 +02:00
|
|
|
|
private static List<ImageOverlay> ReadImageOverlays(XDocument document)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-01 18:21:41 +02:00
|
|
|
|
var rootElement = document.Root;
|
|
|
|
|
|
var ns = rootElement.Name.Namespace;
|
|
|
|
|
|
var docElement = rootElement.Element(ns + "Document") ?? rootElement;
|
|
|
|
|
|
var imageOverlays = new List<ImageOverlay>();
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2024-06-22 08:10:45 +02:00
|
|
|
|
foreach (var folderElement in docElement.Elements(ns + "Folder"))
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var groundOverlayElement in folderElement.Elements(ns + "GroundOverlay"))
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2025-09-04 10:36:39 +02:00
|
|
|
|
var pathElement = groundOverlayElement.Element(ns + "Icon");
|
|
|
|
|
|
var path = pathElement?.Element(ns + "href")?.Value;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2024-09-14 13:26:57 +02:00
|
|
|
|
var latLonBoxElement = groundOverlayElement.Element(ns + "LatLonBox");
|
|
|
|
|
|
var latLonBox = latLonBoxElement != null ? ReadLatLonBox(latLonBoxElement) : null;
|
|
|
|
|
|
|
2024-06-22 08:10:45 +02:00
|
|
|
|
var drawOrder = groundOverlayElement.Element(ns + "drawOrder")?.Value;
|
2025-09-05 08:51:48 +02:00
|
|
|
|
var zIndex = drawOrder != null ? int.Parse(drawOrder) : 0;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2025-09-04 10:36:39 +02:00
|
|
|
|
if (latLonBox != null && path != null)
|
2024-06-22 08:10:45 +02:00
|
|
|
|
{
|
2025-09-05 08:51:48 +02:00
|
|
|
|
imageOverlays.Add(new ImageOverlay(path, latLonBox, zIndex));
|
2024-06-22 08:10:45 +02:00
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-01 18:21:41 +02:00
|
|
|
|
|
|
|
|
|
|
return imageOverlays;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-09 16:44:45 +02:00
|
|
|
|
private static LatLonBox ReadLatLonBox(XElement latLonBoxElement)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2024-06-22 08:10:45 +02:00
|
|
|
|
var ns = latLonBoxElement.Name.Namespace;
|
2022-12-01 22:48:08 +01:00
|
|
|
|
var north = double.NaN;
|
|
|
|
|
|
var south = double.NaN;
|
|
|
|
|
|
var east = double.NaN;
|
|
|
|
|
|
var west = double.NaN;
|
|
|
|
|
|
var rotation = 0d;
|
2022-01-18 21:30:22 +01:00
|
|
|
|
|
2024-06-22 08:10:45 +02:00
|
|
|
|
var value = latLonBoxElement.Element(ns + "north")?.Value;
|
|
|
|
|
|
if (value != null)
|
2022-01-18 21:30:22 +01:00
|
|
|
|
{
|
2024-06-22 08:10:45 +02:00
|
|
|
|
north = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value = latLonBoxElement.Element(ns + "south")?.Value;
|
|
|
|
|
|
if (value != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
south = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value = latLonBoxElement.Element(ns + "east")?.Value;
|
|
|
|
|
|
if (value != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
east = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value = latLonBoxElement.Element(ns + "west")?.Value;
|
|
|
|
|
|
if (value != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
west = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value = latLonBoxElement.Element(ns + "rotation")?.Value;
|
|
|
|
|
|
if (value != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
rotation = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-22 08:10:45 +02:00
|
|
|
|
if (double.IsNaN(north) || double.IsNaN(south) ||
|
|
|
|
|
|
double.IsNaN(east) || double.IsNaN(west) ||
|
|
|
|
|
|
north <= south || east <= west)
|
2022-12-08 15:05:23 +01:00
|
|
|
|
{
|
|
|
|
|
|
throw new FormatException("Invalid LatLonBox");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-09 16:44:45 +02:00
|
|
|
|
return new LatLonBox(south, west, north, east, rotation);
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
2025-09-09 08:18:25 +02:00
|
|
|
|
|
|
|
|
|
|
private static Task<XDocument> LoadXDocument(Stream docStream)
|
|
|
|
|
|
{
|
|
|
|
|
|
#if NETFRAMEWORK
|
|
|
|
|
|
return Task.Run(() => XDocument.Load(docStream, LoadOptions.None));
|
|
|
|
|
|
#else
|
2025-09-09 17:54:43 +02:00
|
|
|
|
return XDocument.LoadAsync(docStream, LoadOptions.None, CancellationToken.None);
|
2025-09-09 08:18:25 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
}
|
2022-01-18 21:30:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|