diff --git a/src/Controls/Maps/src/Map.cs b/src/Controls/Maps/src/Map.cs index 52875e5650ad..27a6a9a165e4 100644 --- a/src/Controls/Maps/src/Map.cs +++ b/src/Controls/Maps/src/Map.cs @@ -42,6 +42,10 @@ public partial class Map : View public static readonly BindableProperty ItemTemplateSelectorProperty = BindableProperty.Create(nameof(ItemTemplateSelector), typeof(DataTemplateSelector), typeof(Map), default(DataTemplateSelector), propertyChanged: (b, o, n) => ((Map)b).OnItemTemplateSelectorPropertyChanged()); + /// Bindable property for . + public static readonly BindableProperty RegionProperty = BindableProperty.Create(nameof(Region), typeof(MapSpan), typeof(Map), null, + propertyChanged: (b, o, n) => ((Map)b).OnRegionPropertyChanged((MapSpan?)n)); + readonly ObservableCollection _pins = new(); readonly ObservableCollection _mapElements = new(); MapSpan? _visibleRegion; @@ -159,6 +163,16 @@ public DataTemplateSelector ItemTemplateSelector set { SetValue(ItemTemplateSelectorProperty, value); } } + /// + /// Gets or sets the region displayed by the map. Setting this property moves the map to the specified region. + /// This is a bindable property. + /// + public MapSpan? Region + { + get { return (MapSpan?)GetValue(RegionProperty); } + set { SetValue(RegionProperty, value); } + } + /// /// Gets the elements (pins, polygons, polylines, etc.) currently added to this map. /// @@ -224,6 +238,14 @@ void SetVisibleRegion(MapSpan? visibleRegion) OnPropertyChanged(nameof(VisibleRegion)); } + void OnRegionPropertyChanged(MapSpan? newRegion) + { + if (newRegion is not null) + { + MoveToRegion(newRegion); + } + } + void PinsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems is not null && e.NewItems.Cast().Any(pin => pin.Label is null)) diff --git a/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 7dc5c58110bf..31029602ec64 100644 --- a/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/Maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +static readonly Microsoft.Maui.Controls.Maps.Map.RegionProperty -> Microsoft.Maui.Controls.BindableProperty! +Microsoft.Maui.Controls.Maps.Map.Region.get -> Microsoft.Maui.Maps.MapSpan? +Microsoft.Maui.Controls.Maps.Map.Region.set -> void diff --git a/src/Controls/tests/Core.UnitTests/MapSpanTypeConverterTests.cs b/src/Controls/tests/Core.UnitTests/MapSpanTypeConverterTests.cs new file mode 100644 index 000000000000..5aaba1efa6f9 --- /dev/null +++ b/src/Controls/tests/Core.UnitTests/MapSpanTypeConverterTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Globalization; +using Microsoft.Maui.Devices.Sensors; +using Microsoft.Maui.Maps; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class MapSpanTypeConverterTests : BaseTestFixture + { + [Fact] + public void ConvertFromValidString() + { + var converter = new MapSpanTypeConverter(); + var result = (MapSpan)converter.ConvertFrom(null, CultureInfo.InvariantCulture, "36.9628,-122.0195,0.01,0.02")!; + + Assert.Equal(36.9628, result.Center.Latitude, 4); + Assert.Equal(-122.0195, result.Center.Longitude, 4); + Assert.Equal(0.01, result.LatitudeDegrees, 10); + Assert.Equal(0.02, result.LongitudeDegrees, 10); + } + + [Fact] + public void ConvertFromWithSpaces() + { + var converter = new MapSpanTypeConverter(); + var result = (MapSpan)converter.ConvertFrom(null, CultureInfo.InvariantCulture, " 36.9628 , -122.0195 , 0.01 , 0.02 ")!; + + Assert.Equal(36.9628, result.Center.Latitude, 4); + Assert.Equal(-122.0195, result.Center.Longitude, 4); + } + + [Fact] + public void ConvertToString() + { + var converter = new MapSpanTypeConverter(); + var span = new MapSpan(new Location(36.9628, -122.0195), 0.01, 0.02); + var result = converter.ConvertTo(null, CultureInfo.InvariantCulture, span, typeof(string)); + + Assert.Equal("36.9628,-122.0195,0.01,0.02", result); + } + + [Fact] + public void ConvertFromInvalidStringThrows() + { + var converter = new MapSpanTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "invalid")); + } + + [Fact] + public void ConvertFromTooFewPartsThrows() + { + var converter = new MapSpanTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "36.9628,-122.0195")); + } + + [Fact] + public void ConvertFromEmptyStringThrows() + { + var converter = new MapSpanTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "")); + } + + [Fact] + public void CanConvertFromString() + { + var converter = new MapSpanTypeConverter(); + Assert.True(converter.CanConvertFrom(null, typeof(string))); + Assert.False(converter.CanConvertFrom(null, typeof(int))); + } + + [Fact] + public void RoundTrip() + { + var converter = new MapSpanTypeConverter(); + var original = new MapSpan(new Location(47.6062, -122.3321), 0.5, 0.5); + var str = (string)converter.ConvertTo(null, CultureInfo.InvariantCulture, original, typeof(string))!; + var result = (MapSpan)converter.ConvertFrom(null, CultureInfo.InvariantCulture, str)!; + + Assert.Equal(original.Center.Latitude, result.Center.Latitude, 10); + Assert.Equal(original.Center.Longitude, result.Center.Longitude, 10); + Assert.Equal(original.LatitudeDegrees, result.LatitudeDegrees, 10); + Assert.Equal(original.LongitudeDegrees, result.LongitudeDegrees, 10); + } + } +} diff --git a/src/Core/maps/src/Converters/MapSpanTypeConverter.cs b/src/Core/maps/src/Converters/MapSpanTypeConverter.cs new file mode 100644 index 000000000000..74ff7f401e25 --- /dev/null +++ b/src/Core/maps/src/Converters/MapSpanTypeConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Microsoft.Maui.Devices.Sensors; + +namespace Microsoft.Maui.Maps +{ + /// + /// A type converter that converts a string representation to a object. + /// + /// + /// Supported formats: + /// + /// "latitude,longitude,latitudeDegrees,longitudeDegrees" (e.g., "36.9628,-122.0195,0.01,0.01") + /// + /// + public class MapSpanTypeConverter : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => sourceType == typeof(string); + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + => destinationType == typeof(string); + + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var strValue = value?.ToString()?.Trim(); + + if (string.IsNullOrEmpty(strValue)) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(MapSpan)}"); + + var parts = strValue.Split(','); + + if (parts.Length == 4 + && double.TryParse(parts[0].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double latitude) + && double.TryParse(parts[1].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double longitude) + && double.TryParse(parts[2].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double latDegrees) + && double.TryParse(parts[3].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double lonDegrees)) + { + return new MapSpan(new Location(latitude, longitude), latDegrees, lonDegrees); + } + + throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(MapSpan)}"); + } + + /// + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is not MapSpan span) + throw new NotSupportedException(); + + return $"{span.Center.Latitude.ToString(CultureInfo.InvariantCulture)},{span.Center.Longitude.ToString(CultureInfo.InvariantCulture)}," + + $"{span.LatitudeDegrees.ToString(CultureInfo.InvariantCulture)},{span.LongitudeDegrees.ToString(CultureInfo.InvariantCulture)}"; + } + } +} diff --git a/src/Core/maps/src/Primitives/MapSpan.cs b/src/Core/maps/src/Primitives/MapSpan.cs index d5e7fe6d7d92..49ed0d191b5d 100644 --- a/src/Core/maps/src/Primitives/MapSpan.cs +++ b/src/Core/maps/src/Primitives/MapSpan.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Microsoft.Maui.Devices.Sensors; namespace Microsoft.Maui.Maps @@ -6,6 +7,7 @@ namespace Microsoft.Maui.Maps /// /// Represents a rectangular region on the map, defined by a center point and span. /// + [TypeConverter(typeof(MapSpanTypeConverter))] public sealed class MapSpan { const double EarthCircumferenceKm = GeographyUtils.EarthRadiusKm * 2 * Math.PI; diff --git a/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 7dc5c58110bf..9bd77ba275b9 100644 --- a/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Maps.MapSpanTypeConverter +Microsoft.Maui.Maps.MapSpanTypeConverter.MapSpanTypeConverter() -> void +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? +override Microsoft.Maui.Maps.MapSpanTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 7dc5c58110bf..9939e6dfc2d4 100644 --- a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Maui.Devices.Sensors.LocationTypeConverter +Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool +override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? +~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? diff --git a/src/Essentials/src/Types/Location.shared.cs b/src/Essentials/src/Types/Location.shared.cs index 892cf582d4f3..98df2068b11e 100644 --- a/src/Essentials/src/Types/Location.shared.cs +++ b/src/Essentials/src/Types/Location.shared.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Microsoft.Maui.Media; namespace Microsoft.Maui.Devices.Sensors @@ -37,6 +38,7 @@ public enum AltitudeReferenceSystem /// /// Represents a physical location with the latitude, longitude, altitude and time information reported by the device. /// + [TypeConverter(typeof(LocationTypeConverter))] public class Location { /// diff --git a/src/Essentials/src/Types/LocationTypeConverter.shared.cs b/src/Essentials/src/Types/LocationTypeConverter.shared.cs new file mode 100644 index 000000000000..f212c4354781 --- /dev/null +++ b/src/Essentials/src/Types/LocationTypeConverter.shared.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Microsoft.Maui.Devices.Sensors +{ + /// + /// A type converter that converts a string representation of latitude and longitude to a object. + /// + /// + /// Supported formats: + /// + /// "latitude,longitude" (e.g., "36.9628066,-122.0194722") + /// + /// + public class LocationTypeConverter : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => sourceType == typeof(string); + + /// + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + => destinationType == typeof(string); + + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var strValue = value?.ToString()?.Trim(); + + if (string.IsNullOrEmpty(strValue)) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Location)}"); + + var parts = strValue.Split(','); + + if (parts.Length == 2 + && double.TryParse(parts[0].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double latitude) + && double.TryParse(parts[1].Trim(), NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out double longitude)) + { + return new Location(latitude, longitude); + } + + throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(Location)}"); + } + + /// + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is not Location location) + throw new NotSupportedException(); + + return $"{location.Latitude.ToString(CultureInfo.InvariantCulture)},{location.Longitude.ToString(CultureInfo.InvariantCulture)}"; + } + } +} diff --git a/src/Essentials/test/UnitTests/LocationTypeConverter_Tests.cs b/src/Essentials/test/UnitTests/LocationTypeConverter_Tests.cs new file mode 100644 index 000000000000..f37b0c88a49b --- /dev/null +++ b/src/Essentials/test/UnitTests/LocationTypeConverter_Tests.cs @@ -0,0 +1,112 @@ +using System; +using System.Globalization; +using Microsoft.Maui.Devices.Sensors; +using Xunit; + +namespace Tests +{ + public class LocationTypeConverter_Tests + { + [Fact] + public void ConvertFromValidString() + { + var converter = new LocationTypeConverter(); + var result = (Location)converter.ConvertFrom(null, CultureInfo.InvariantCulture, "36.9628066,-122.0194722")!; + + Assert.Equal(36.9628066, result.Latitude, 7); + Assert.Equal(-122.0194722, result.Longitude, 7); + } + + [Fact] + public void ConvertFromWithSpaces() + { + var converter = new LocationTypeConverter(); + var result = (Location)converter.ConvertFrom(null, CultureInfo.InvariantCulture, " 36.9628066 , -122.0194722 ")!; + + Assert.Equal(36.9628066, result.Latitude, 7); + Assert.Equal(-122.0194722, result.Longitude, 7); + } + + [Fact] + public void ConvertFromNegativeValues() + { + var converter = new LocationTypeConverter(); + var result = (Location)converter.ConvertFrom(null, CultureInfo.InvariantCulture, "-33.8688,151.2093")!; + + Assert.Equal(-33.8688, result.Latitude, 4); + Assert.Equal(151.2093, result.Longitude, 4); + } + + [Fact] + public void ConvertFromZero() + { + var converter = new LocationTypeConverter(); + var result = (Location)converter.ConvertFrom(null, CultureInfo.InvariantCulture, "0,0")!; + + Assert.Equal(0, result.Latitude); + Assert.Equal(0, result.Longitude); + } + + [Fact] + public void ConvertToString() + { + var converter = new LocationTypeConverter(); + var location = new Location(36.9628066, -122.0194722); + var result = converter.ConvertTo(null, CultureInfo.InvariantCulture, location, typeof(string)); + + Assert.Equal("36.9628066,-122.0194722", result); + } + + [Fact] + public void ConvertFromInvalidStringThrows() + { + var converter = new LocationTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "invalid")); + } + + [Fact] + public void ConvertFromEmptyStringThrows() + { + var converter = new LocationTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "")); + } + + [Fact] + public void ConvertFromTooManyPartsThrows() + { + var converter = new LocationTypeConverter(); + Assert.Throws(() => + converter.ConvertFrom(null, CultureInfo.InvariantCulture, "1,2,3")); + } + + [Fact] + public void CanConvertFromString() + { + var converter = new LocationTypeConverter(); + Assert.True(converter.CanConvertFrom(null, typeof(string))); + Assert.False(converter.CanConvertFrom(null, typeof(int))); + } + + [Fact] + public void CanConvertToString() + { + var converter = new LocationTypeConverter(); + Assert.True(converter.CanConvertTo(null, typeof(string))); + Assert.False(converter.CanConvertTo(null, typeof(int))); + } + + [Fact] + public void RoundTrip() + { + var converter = new LocationTypeConverter(); + var original = new Location(47.6062, -122.3321); + var str = (string)converter.ConvertTo(null, CultureInfo.InvariantCulture, original, typeof(string))!; + var result = (Location)converter.ConvertFrom(null, CultureInfo.InvariantCulture, str)!; + + Assert.Equal(original.Latitude, result.Latitude, 10); + Assert.Equal(original.Longitude, result.Longitude, 10); + } + } +}