From 135473b7991bb35a96c85b8a0848a595b5cef81c Mon Sep 17 00:00:00 2001 From: Clemens Date: Tue, 18 Jan 2022 21:30:22 +0100 Subject: [PATCH] Added GroundOverlayPanel --- MapControl/Shared/GroundOverlayPanel.cs | 276 ++++++++++++++++++++++++ MapControl/UWP/MapControl.UWP.csproj | 3 + MapControl/WPF/MapControl.WPF.csproj | 1 + MapControlExtended.sln | 61 +----- 4 files changed, 281 insertions(+), 60 deletions(-) create mode 100644 MapControl/Shared/GroundOverlayPanel.cs diff --git a/MapControl/Shared/GroundOverlayPanel.cs b/MapControl/Shared/GroundOverlayPanel.cs new file mode 100644 index 00000000..9bb2bff7 --- /dev/null +++ b/MapControl/Shared/GroundOverlayPanel.cs @@ -0,0 +1,276 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2022 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +#if WINUI +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +#elif UWP +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +#else +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +#endif + +namespace MapControl +{ + public class GroundOverlayPanel : MapPanel + { + class LatLonBox : BoundingBox + { + public LatLonBox(double south, double west, double north, double east, double rotation) + : base(south, west, north, east) + { + Rotation = rotation; + } + + public double Rotation { get; } + } + + class ImageOverlay + { + public ImageOverlay(LatLonBox latLonBox, string imagePath, int zIndex) + { + LatLonBox = latLonBox; + ImagePath = imagePath; + ZIndex = zIndex; + } + + public LatLonBox LatLonBox { get; } + public string ImagePath { get; } + public int ZIndex { get; } + public ImageSource ImageSource { get; set; } + } + + public static readonly DependencyProperty KmlFileProperty = DependencyProperty.Register( + nameof(KmlFile), typeof(string), typeof(GroundOverlayPanel), + new PropertyMetadata(null, async (o, e) => await ((GroundOverlayPanel)o).KmlFilePropertyChanged((string)e.NewValue))); + + public string KmlFile + { + get { return (string)GetValue(KmlFileProperty); } + set { SetValue(KmlFileProperty, value); } + } + + private async Task KmlFilePropertyChanged(string path) + { + IEnumerable imageOverlays = null; + + if (!string.IsNullOrEmpty(path)) + { + try + { + var ext = Path.GetExtension(path).ToLower(); + if (ext == ".kmz") + { + imageOverlays = await ReadGroundOverlaysFromArchiveAsync(path); + } + else if (ext == ".kml") + { + imageOverlays = await ReadGroundOverlaysFromFileAsync(path); + } + } + catch (Exception ex) + { + Debug.WriteLine($"GroundOverlayPanel: {path}: {ex.Message}"); + } + } + + Children.Clear(); + + if (imageOverlays != null) + { + AddImageOverlays(imageOverlays); + } + } + + private void AddImageOverlays(IEnumerable imageOverlays) + { + foreach (var imageOverlay in imageOverlays.Where(i => i.ImageSource != null)) + { + FrameworkElement overlay = new Image + { + Source = imageOverlay.ImageSource, + Stretch = Stretch.Fill, + UseLayoutRounding = false + }; + + if (imageOverlay.LatLonBox.Rotation != 0d) + { + overlay.RenderTransform = new RotateTransform { Angle = -imageOverlay.LatLonBox.Rotation }; + overlay.RenderTransformOrigin = new Point(0.5, 0.5); + + // additional Panel for map rotation, see MapPanel.ArrangeElementWithBoundingBox + var panel = new Grid { UseLayoutRounding = false }; + panel.Children.Add(overlay); + overlay = panel; + } + + SetBoundingBox(overlay, imageOverlay.LatLonBox); + Canvas.SetZIndex(overlay, imageOverlay.ZIndex); + Children.Add(overlay); + } + } + + private static async Task> ReadGroundOverlaysFromArchiveAsync(string archiveFile) + { + using (var archive = await Task.Run(() => ZipFile.OpenRead(archiveFile))) + { + var docEntry = await Task.Run(() => archive.GetEntry("doc.kml") ?? archive.Entries.FirstOrDefault(e => e.Name.EndsWith(".kml"))); + + if (docEntry == null) + { + throw new ArgumentException("No KML entry found in " + archiveFile); + } + + var imageOverlays = await Task.Run(() => + { + var kmlDocument = new XmlDocument(); + + using (var docStream = docEntry.Open()) + { + kmlDocument.Load(docStream); + } + + return ReadGroundOverlays(kmlDocument).ToList(); + }); + + foreach (var imageOverlay in imageOverlays) + { + var imageEntry = await Task.Run(() => archive.GetEntry(imageOverlay.ImagePath)); + + if (imageEntry != null) + { + using (var zipStream = imageEntry.Open()) + using (var memoryStream = new MemoryStream()) + { + await zipStream.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + imageOverlay.ImageSource = await ImageLoader.LoadImageAsync(memoryStream); + } + } + } + + return imageOverlays; + } + } + + private static async Task> ReadGroundOverlaysFromFileAsync(string docFile) + { + docFile = Path.GetFullPath(docFile); + var docUri = new Uri(docFile); + + var imageOverlays = await Task.Run(() => + { + var kmlDocument = new XmlDocument(); + kmlDocument.Load(docFile); + + return ReadGroundOverlays(kmlDocument).ToList(); + }); + + foreach (var imageOverlay in imageOverlays) + { + imageOverlay.ImageSource = await ImageLoader.LoadImageAsync(new Uri(docUri, imageOverlay.ImagePath)); + } + + return imageOverlays; + } + + private static IEnumerable ReadGroundOverlays(XmlDocument kmlDocument) + { + foreach (XmlElement groundOverlayElement in kmlDocument.GetElementsByTagName("GroundOverlay")) + { + LatLonBox latLonBox = null; + string imagePath = null; + int zIndex = 0; + + foreach (var childElement in groundOverlayElement.ChildNodes.OfType()) + { + switch (childElement.LocalName) + { + case "LatLonBox": + latLonBox = ReadLatLonBox(childElement); + break; + case "Icon": + imagePath = ReadImagePath(childElement); + break; + case "drawOrder": + int.TryParse(childElement.InnerText.Trim(), out zIndex); + break; + } + } + + if (latLonBox != null && imagePath != null) + { + yield return new ImageOverlay(latLonBox, imagePath, zIndex); + } + } + } + + private static string ReadImagePath(XmlElement element) + { + string href = null; + + foreach (var childElement in element.ChildNodes.OfType()) + { + switch (childElement.LocalName) + { + case "href": + href = childElement.InnerText.Trim(); + break; + } + } + + return href; + } + + private static LatLonBox ReadLatLonBox(XmlElement element) + { + double north = double.NaN; + double south = double.NaN; + double east = double.NaN; + double west = double.NaN; + double rotation = 0d; + + foreach (var childElement in element.ChildNodes.OfType()) + { + switch (childElement.LocalName) + { + case "north": + double.TryParse(childElement.InnerText.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out north); + break; + case "south": + double.TryParse(childElement.InnerText.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out south); + break; + case "east": + double.TryParse(childElement.InnerText.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out east); + break; + case "west": + double.TryParse(childElement.InnerText.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out west); + break; + case "rotation": + double.TryParse(childElement.InnerText.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation); + break; + } + } + + return !double.IsNaN(north) && !double.IsNaN(south) && !double.IsNaN(east) && !double.IsNaN(west) + ? new LatLonBox(south, west, north, east, rotation) + : null; + } + } +} diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index e29802a1..4227aff6 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -77,6 +77,9 @@ GnomonicProjection.cs + + GroundOverlayPanel.cs + ImageFileCache.cs diff --git a/MapControl/WPF/MapControl.WPF.csproj b/MapControl/WPF/MapControl.WPF.csproj index 6bab31a8..f0111efd 100644 --- a/MapControl/WPF/MapControl.WPF.csproj +++ b/MapControl/WPF/MapControl.WPF.csproj @@ -33,6 +33,7 @@ + diff --git a/MapControlExtended.sln b/MapControlExtended.sln index 7d117f6c..17f57754 100644 --- a/MapControlExtended.sln +++ b/MapControlExtended.sln @@ -33,12 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApplication", "SampleApp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapProjections.UWP", "MapProjections\UWP\MapProjections.UWP.csproj", "{9EE69591-5EDC-45E3-893E-2F9A4B82D538}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MapImages", "MapImages", "{2FDC8B91-FB95-4C57-8183-63587FBFE180}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapImages.WPF", "MapImages\WPF\MapImages.WPF.csproj", "{52042F63-563A-45BB-9A08-A8635AAAB84C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapImages.UWP", "MapImages\UWP\MapImages.UWP.csproj", "{BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SQLiteCache", "SQLiteCache", "{96FD1258-1597-48A2-8D64-1ADAE13E886A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteCache.UWP", "SQLiteCache\UWP\SQLiteCache.UWP.csproj", "{56DFA7CF-F31D-45CE-9C36-DA8DBB8413B1}" @@ -53,8 +47,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SQLiteCache.WinUI", "SQLite EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapProjections.WinUI", "MapProjections\WinUI\MapProjections.WinUI.csproj", "{3572F71A-83FE-459D-8370-002CA28827FE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapImages.WinUI", "MapImages\WinUI\MapImages.WinUI.csproj", "{1F9FBADF-65C0-453D-9B45-7A88044F807F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MBTiles.WinUI", "MBTiles\WinUI\MBTiles.WinUI.csproj", "{817D606F-A22D-485C-89CF-86062C8E97EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinUiApp", "SampleApps\WinUiApp\WinUiApp.csproj", "{751EF297-7CF4-4879-BA8F-42661FA68668}" @@ -69,7 +61,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapUiTools.WinUI", "MapUiTo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapUiTools.WPF", "MapUiTools\WPF\MapUiTools.WPF.csproj", "{12430DAE-DC53-4C37-95D5-B8923B5FD3D7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maps.WinUI", "SampleApps\Maps.WinUI\Maps.WinUI.csproj", "{780437BF-C773-448B-989E-6FB13A7B1F49}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Maps.WinUI", "SampleApps\Maps.WinUI\Maps.WinUI.csproj", "{780437BF-C773-448B-989E-6FB13A7B1F49}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -251,38 +243,6 @@ Global {9EE69591-5EDC-45E3-893E-2F9A4B82D538}.Release|x64.Build.0 = Release|Any CPU {9EE69591-5EDC-45E3-893E-2F9A4B82D538}.Release|x86.ActiveCfg = Release|Any CPU {9EE69591-5EDC-45E3-893E-2F9A4B82D538}.Release|x86.Build.0 = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|arm64.ActiveCfg = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|arm64.Build.0 = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|x64.ActiveCfg = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|x64.Build.0 = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|x86.ActiveCfg = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Debug|x86.Build.0 = Debug|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|Any CPU.Build.0 = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|arm64.ActiveCfg = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|arm64.Build.0 = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|x64.ActiveCfg = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|x64.Build.0 = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|x86.ActiveCfg = Release|Any CPU - {52042F63-563A-45BB-9A08-A8635AAAB84C}.Release|x86.Build.0 = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|arm64.ActiveCfg = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|arm64.Build.0 = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|x64.ActiveCfg = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|x64.Build.0 = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|x86.ActiveCfg = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Debug|x86.Build.0 = Debug|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|Any CPU.Build.0 = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|arm64.ActiveCfg = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|arm64.Build.0 = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|x64.ActiveCfg = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|x64.Build.0 = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|x86.ActiveCfg = Release|Any CPU - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372}.Release|x86.Build.0 = Release|Any CPU {56DFA7CF-F31D-45CE-9C36-DA8DBB8413B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {56DFA7CF-F31D-45CE-9C36-DA8DBB8413B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {56DFA7CF-F31D-45CE-9C36-DA8DBB8413B1}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -379,22 +339,6 @@ Global {3572F71A-83FE-459D-8370-002CA28827FE}.Release|x64.Build.0 = Release|Any CPU {3572F71A-83FE-459D-8370-002CA28827FE}.Release|x86.ActiveCfg = Release|Any CPU {3572F71A-83FE-459D-8370-002CA28827FE}.Release|x86.Build.0 = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|arm64.ActiveCfg = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|arm64.Build.0 = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|x64.ActiveCfg = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|x64.Build.0 = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|x86.ActiveCfg = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Debug|x86.Build.0 = Debug|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|Any CPU.Build.0 = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|arm64.ActiveCfg = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|arm64.Build.0 = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|x64.ActiveCfg = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|x64.Build.0 = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|x86.ActiveCfg = Release|Any CPU - {1F9FBADF-65C0-453D-9B45-7A88044F807F}.Release|x86.Build.0 = Release|Any CPU {817D606F-A22D-485C-89CF-86062C8E97EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {817D606F-A22D-485C-89CF-86062C8E97EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {817D606F-A22D-485C-89CF-86062C8E97EF}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -538,15 +482,12 @@ Global {426C21C0-5F14-491F-BCD1-6D2993510420} = {7BC11E28-8D3B-4C5B-AC08-AB249CC95F6D} {F92DA93D-75DB-4308-A5F9-6B4C3908A675} = {8F2103C2-78AF-4810-8FB9-67572F50C8FC} {9EE69591-5EDC-45E3-893E-2F9A4B82D538} = {7BC11E28-8D3B-4C5B-AC08-AB249CC95F6D} - {52042F63-563A-45BB-9A08-A8635AAAB84C} = {2FDC8B91-FB95-4C57-8183-63587FBFE180} - {BE08B7BC-8C89-4837-BCE7-EDDDABEAB372} = {2FDC8B91-FB95-4C57-8183-63587FBFE180} {56DFA7CF-F31D-45CE-9C36-DA8DBB8413B1} = {96FD1258-1597-48A2-8D64-1ADAE13E886A} {0109C2F0-BA2C-420F-B2CA-DB5B29B1A349} = {96FD1258-1597-48A2-8D64-1ADAE13E886A} {ACA8E56C-0F82-4010-A83E-2DBFF5D16919} = {52AECE49-F314-4F76-98F2-FA800F07824B} {DFE09FD5-530D-48AB-8A46-4611F21BBBC3} = {261905DE-9653-4567-B498-1F46BEA2A4F3} {E33FC359-F713-462C-8A8E-7EEA15E36BE1} = {96FD1258-1597-48A2-8D64-1ADAE13E886A} {3572F71A-83FE-459D-8370-002CA28827FE} = {7BC11E28-8D3B-4C5B-AC08-AB249CC95F6D} - {1F9FBADF-65C0-453D-9B45-7A88044F807F} = {2FDC8B91-FB95-4C57-8183-63587FBFE180} {817D606F-A22D-485C-89CF-86062C8E97EF} = {CEAD0EA1-A971-4F5F-9EAE-C72F75D1F737} {751EF297-7CF4-4879-BA8F-42661FA68668} = {8F2103C2-78AF-4810-8FB9-67572F50C8FC} {AC8C7BE0-9E72-434B-8BF3-FAEFAC2E859C} = {8F2103C2-78AF-4810-8FB9-67572F50C8FC}