From 9d368d7ee892150ddb5c0e4774a3e7dde8d92a83 Mon Sep 17 00:00:00 2001 From: Clemens Date: Tue, 18 Jan 2022 18:07:19 +0100 Subject: [PATCH] Added GeoImage --- MapControl/Shared/GeoImage.cs | 155 +++++++++++++++++++++++++++ MapControl/Shared/ImageLoader.cs | 2 +- MapControl/UWP/MapControl.UWP.csproj | 6 ++ MapControl/WPF/GeoImage.WPF.cs | 102 +++++++++++++++--- MapControl/WinUI/GeoImage.WinUI.cs | 76 +++++++++++++ 5 files changed, 323 insertions(+), 18 deletions(-) create mode 100644 MapControl/Shared/GeoImage.cs create mode 100644 MapControl/WinUI/GeoImage.WinUI.cs diff --git a/MapControl/Shared/GeoImage.cs b/MapControl/Shared/GeoImage.cs new file mode 100644 index 00000000..fd656f9d --- /dev/null +++ b/MapControl/Shared/GeoImage.cs @@ -0,0 +1,155 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +#if WINUI +using Windows.Foundation; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +#elif UWP +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; +#else +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +#endif + +namespace MapControl +{ + public partial class GeoImage : ContentControl + { + private const string PixelScaleQuery = "/ifd/{ushort=33550}"; + private const string TiePointQuery = "/ifd/{ushort=33922}"; + private const string TransformQuery = "/ifd/{ushort=34264}"; + private const string NoDataQuery = "/ifd/{ushort=42113}"; + + public static readonly DependencyProperty SourceUriProperty = DependencyProperty.Register( + nameof(SourceUri), typeof(Uri), typeof(GeoImage), + new PropertyMetadata(null, async (o, e) => await ((GeoImage)o).SourceUriPropertyChanged((Uri)e.NewValue))); + + public Uri SourceUri + { + get { return (Uri)GetValue(SourceUriProperty); } + set { SetValue(SourceUriProperty, value); } + } + + public GeoImage() + { + HorizontalContentAlignment = HorizontalAlignment.Stretch; + VerticalContentAlignment = VerticalAlignment.Stretch; + } + + private async Task SourceUriPropertyChanged(Uri sourceUri) + { + Image image = null; + BoundingBox boundingBox = null; + + if (sourceUri != null) + { + Tuple geoBitmap = null; + + if (!sourceUri.IsAbsoluteUri || sourceUri.IsFile) + { + var imageFilePath = sourceUri.IsAbsoluteUri ? sourceUri.LocalPath : sourceUri.OriginalString; + var ext = Path.GetExtension(imageFilePath); + + if (ext.Length >= 4) + { + var dir = Path.GetDirectoryName(imageFilePath); + var file = Path.GetFileNameWithoutExtension(imageFilePath); + var worldFilePath = Path.Combine(dir, file + ext.Remove(2, 1) + "w"); + + if (File.Exists(worldFilePath)) + { + geoBitmap = await ReadWorldFileImage(imageFilePath, worldFilePath); + } + } + } + + if (geoBitmap == null) + { + geoBitmap = await ReadGeoTiff(sourceUri); + } + + var bitmap = geoBitmap.Item1; + var transform = geoBitmap.Item2; + + image = new Image + { + Source = bitmap, + Stretch = Stretch.Fill + }; + + if (transform.M12 != 0 || transform.M21 != 0) + { + var rotation = (Math.Atan2(transform.M12, transform.M11) + Math.Atan2(transform.M21, -transform.M22)) * 90d / Math.PI; + + image.RenderTransform = new RotateTransform { Angle = -rotation }; + + // effective unrotated transform + transform.M11 = Math.Sqrt(transform.M11 * transform.M11 + transform.M12 * transform.M12); + transform.M22 = -Math.Sqrt(transform.M22 * transform.M22 + transform.M21 * transform.M21); + transform.M12 = 0; + transform.M21 = 0; + } + + var rect = new Rect( + transform.Transform(new Point()), + transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight))); + + boundingBox = new BoundingBox(rect.Y, rect.X, rect.Y + rect.Height, rect.X + rect.Width); + } + + Content = image; + + MapPanel.SetBoundingBox(this, boundingBox); + } + + private static async Task> ReadWorldFileImage(string imageFilePath, string worldFilePath) + { + var bitmap = (BitmapSource)await ImageLoader.LoadImageAsync(imageFilePath); + + var transform = await Task.Run(() => + { + var parameters = File.ReadLines(worldFilePath) + .Take(6) + .Select((line, i) => + { + if (!double.TryParse(line, NumberStyles.Float, CultureInfo.InvariantCulture, out double parameter)) + { + throw new ArgumentException("Failed parsing line " + (i + 1) + " in world file \"" + worldFilePath + "\"."); + } + return parameter; + }) + .ToList(); + + if (parameters.Count != 6) + { + throw new ArgumentException("Insufficient number of parameters in world file \"" + worldFilePath + "\"."); + } + + return new Matrix( + parameters[0], // line 1: A or M11 + parameters[1], // line 2: D or M12 + parameters[2], // line 3: B or M21 + parameters[3], // line 4: E or M22 + parameters[4], // line 5: C or OffsetX + parameters[5]); // line 6: F or OffsetY + }); + + return new Tuple(bitmap, transform); + } + } +} diff --git a/MapControl/Shared/ImageLoader.cs b/MapControl/Shared/ImageLoader.cs index ab9b94f7..b7137d2a 100644 --- a/MapControl/Shared/ImageLoader.cs +++ b/MapControl/Shared/ImageLoader.cs @@ -35,7 +35,7 @@ namespace MapControl try { - if (!uri.IsAbsoluteUri || uri.Scheme == "file") + if (!uri.IsAbsoluteUri || uri.IsFile) { image = await LoadImageAsync(uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString); } diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index e386b499..e29802a1 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -71,6 +71,9 @@ EquirectangularProjection.cs + + GeoImage.cs + GnomonicProjection.cs @@ -185,6 +188,9 @@ Animatable.WinUI.cs + + GeoImage.WinUI.cs + ImageFileCache.WinUI.cs diff --git a/MapControl/WPF/GeoImage.WPF.cs b/MapControl/WPF/GeoImage.WPF.cs index e1a61157..74502f67 100644 --- a/MapControl/WPF/GeoImage.WPF.cs +++ b/MapControl/WPF/GeoImage.WPF.cs @@ -1,34 +1,102 @@ -using System; -using System.Collections.Generic; +// 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.Linq; -using System.Text; using System.Threading.Tasks; -using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; namespace MapControl { - public partial class GeoImage : FrameworkElement + public partial class GeoImage { - - - public BitmapSource Source + private static async Task> ReadGeoTiff(Uri sourceUri) { - get { return (BitmapSource)GetValue(SourceProperty); } - set { SetValue(SourceProperty, value); } + return await Task.Run(() => + { + BitmapSource bitmap = BitmapFrame.Create(sourceUri, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + var metadata = (BitmapMetadata)bitmap.Metadata; + Matrix transform; + + if (metadata.GetQuery(PixelScaleQuery) is double[] pixelScale && pixelScale.Length == 3 && + metadata.GetQuery(TiePointQuery) is double[] tiePoint && tiePoint.Length >= 6) + { + transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]); + } + else if (metadata.GetQuery(TransformQuery) is double[] tform && tform.Length == 16) + { + transform = new Matrix(tform[0], tform[1], tform[4], tform[5], tform[3], tform[7]); + } + else + { + throw new ArgumentException("No coordinate transformation found in \"" + sourceUri + "\"."); + } + + if (metadata.GetQuery(NoDataQuery) is string noData && int.TryParse(noData, out int noDataValue)) + { + bitmap = ConvertTransparentPixel(bitmap, noDataValue); + } + + return new Tuple(bitmap, transform); + }); } - public static readonly DependencyProperty SourceProperty = - DependencyProperty.Register(nameof(Source), typeof(BitmapSource), typeof(GeoImage), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); - - protected override void OnRender(DrawingContext drawingContext) + private static BitmapSource ConvertTransparentPixel(BitmapSource source, int transparentPixel) { - if (Source != null) + BitmapPalette sourcePalette = null; + var targetFormat = source.Format; + + if (source.Format == PixelFormats.Indexed8 || + source.Format == PixelFormats.Indexed4 || + source.Format == PixelFormats.Indexed2 || + source.Format == PixelFormats.Indexed1) { - drawingContext.DrawImage(Source, new Rect(RenderSize)); + sourcePalette = source.Palette; } + else if (source.Format == PixelFormats.Gray8) + { + sourcePalette = BitmapPalettes.Gray256; + targetFormat = PixelFormats.Indexed8; + } + else if (source.Format == PixelFormats.Gray4) + { + sourcePalette = BitmapPalettes.Gray16; + targetFormat = PixelFormats.Indexed4; + } + else if (source.Format == PixelFormats.Gray2) + { + sourcePalette = BitmapPalettes.Gray4; + targetFormat = PixelFormats.Indexed2; + } + else if (source.Format == PixelFormats.BlackWhite) + { + sourcePalette = BitmapPalettes.BlackAndWhite; + targetFormat = PixelFormats.Indexed1; + } + + if (sourcePalette == null || transparentPixel >= sourcePalette.Colors.Count) + { + return source; + } + + var colors = sourcePalette.Colors.ToList(); + + colors[transparentPixel] = Colors.Transparent; + + var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8; + var buffer = new byte[stride * source.PixelHeight]; + + source.CopyPixels(buffer, stride, 0); + + var target = BitmapSource.Create( + source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY, + targetFormat, new BitmapPalette(colors), buffer, stride); + + target.Freeze(); + + return target; } } } diff --git a/MapControl/WinUI/GeoImage.WinUI.cs b/MapControl/WinUI/GeoImage.WinUI.cs new file mode 100644 index 00000000..5b36bf86 --- /dev/null +++ b/MapControl/WinUI/GeoImage.WinUI.cs @@ -0,0 +1,76 @@ +// 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.IO; +using System.Threading.Tasks; +using Windows.Graphics.Imaging; +using Windows.Storage; +#if WINUI +using Microsoft.UI.Xaml.Media.Imaging; +#else +using Windows.UI.Xaml.Media.Imaging; +#endif + +namespace MapControl +{ + public partial class GeoImage + { + public static async Task> ReadGeoTiff(Uri sourceUri) + { + StorageFile file; + + if (!sourceUri.IsAbsoluteUri || sourceUri.IsFile) + { + file = await StorageFile.GetFileFromPathAsync( + sourceUri.IsAbsoluteUri ? sourceUri.LocalPath : Path.GetFullPath(sourceUri.OriginalString)); + } + else + { + file = await StorageFile.GetFileFromApplicationUriAsync(sourceUri); + } + + using (var stream = await file.OpenReadAsync()) + { + WriteableBitmap bitmap; + Matrix transform; + + var decoder = await BitmapDecoder.CreateAsync(stream); + + using (var swbmp = await decoder.GetSoftwareBitmapAsync()) + { + bitmap = new WriteableBitmap(swbmp.PixelWidth, swbmp.PixelHeight); + swbmp.CopyToBuffer(bitmap.PixelBuffer); + } + + var query = new List + { + PixelScaleQuery, TiePointQuery, TransformQuery, NoDataQuery + }; + + var metadata = await decoder.BitmapProperties.GetPropertiesAsync(query); + + if (metadata.TryGetValue(PixelScaleQuery, out BitmapTypedValue pixelScaleValue) && + pixelScaleValue.Value is double[] pixelScale && pixelScale.Length == 3 && + metadata.TryGetValue(TiePointQuery, out BitmapTypedValue tiePointValue) && + tiePointValue.Value is double[] tiePoint && tiePoint.Length >= 6) + { + transform = new Matrix(pixelScale[0], 0d, 0d, -pixelScale[1], tiePoint[3], tiePoint[4]); + } + else if (metadata.TryGetValue(TransformQuery, out BitmapTypedValue tformValue) && + tformValue.Value is double[] tform && tform.Length == 16) + { + transform = new Matrix(tform[0], tform[1], tform[4], tform[5], tform[3], tform[7]); + } + else + { + throw new ArgumentException("No coordinate transformation found in \"" + sourceUri + "\"."); + } + + return new Tuple(bitmap, transform); + } + } + } +}