Changed class Location to readonly struct

This commit is contained in:
ClemensFischer 2026-02-01 22:56:50 +01:00
parent d7d7bba5f2
commit 6566167ff0
27 changed files with 194 additions and 307 deletions

View file

@ -5,35 +5,20 @@ namespace MapControl
{
/// <summary>
/// A geographic location with latitude and longitude values in degrees.
/// For calculations with azimuth and distance on great circles, see
/// https://en.wikipedia.org/wiki/Great_circle,
/// https://en.wikipedia.org/wiki/Great-circle_distance,
/// https://en.wikipedia.org/wiki/Great-circle_navigation.
/// </summary>
#if UWP || WINUI
[Windows.Foundation.Metadata.CreateFromString(MethodName = "Parse")]
#else
[System.ComponentModel.TypeConverter(typeof(LocationConverter))]
#endif
public class Location : IEquatable<Location>
public readonly struct Location(double latitude, double longitude) : IEquatable<Location>
{
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
//
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
public double Latitude { get; } = Math.Min(Math.Max(latitude, -90d), 90d);
public double Longitude => longitude;
public Location()
{
}
public static bool operator ==(Location loc1, Location loc2) => loc1.Equals(loc2);
public Location(double latitude, double longitude)
{
Latitude = Math.Min(Math.Max(latitude, -90d), 90d);
Longitude = longitude;
}
public double Latitude { get; }
public double Longitude { get; }
public static bool operator !=(Location loc1, Location loc2) => !loc1.Equals(loc2);
public bool LatitudeEquals(double latitude) => Math.Abs(Latitude - latitude) < 1e-9;
@ -41,9 +26,9 @@ namespace MapControl
public bool Equals(double latitude, double longitude) => LatitudeEquals(latitude) && LongitudeEquals(longitude);
public bool Equals(Location location) => location != null && Equals(location.Latitude, location.Longitude);
public bool Equals(Location location) => Equals(location.Latitude, location.Longitude);
public override bool Equals(object obj) => Equals(obj as Location);
public override bool Equals(object obj) => obj is Location location && Equals(location);
public override int GetHashCode() => Latitude.GetHashCode() ^ Longitude.GetHashCode();
@ -81,8 +66,14 @@ namespace MapControl
return x < 0d ? x + 180d : x - 180d;
}
// Arithmetic mean radius (2*a + b) / 3 == (1 - f/3) * a.
// See https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius.
//
public const double Wgs84MeanRadius = (1d - MapProjection.Wgs84Flattening / 3d) * MapProjection.Wgs84EquatorialRadius;
/// <summary>
/// Calculates great circle azimuth in degrees and distance in meters between this and the specified Location.
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
/// </summary>
public (double, double) GetAzimuthDistance(Location location, double earthRadius = Wgs84MeanRadius)
{
@ -108,6 +99,7 @@ namespace MapControl
/// <summary>
/// Calculates great distance in meters between this and the specified Location.
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Course.
/// </summary>
public double GetDistance(Location location, double earthRadius = Wgs84MeanRadius)
{
@ -118,6 +110,7 @@ namespace MapControl
/// <summary>
/// Calculates the Location on a great circle at the specified azimuth in degrees and distance in meters from this Location.
/// See https://en.wikipedia.org/wiki/Great-circle_navigation#Finding_way-points.
/// </summary>
public Location GetLocation(double azimuth, double distance, double earthRadius = Wgs84MeanRadius)
{

View file

@ -43,7 +43,7 @@ namespace MapControl
DependencyPropertyHelper.Register<MapBase, MapProjection>(nameof(MapProjection), new WebMercatorProjection(),
(map, oldValue, newValue) => map.MapProjectionPropertyChanged(newValue));
private Location transformCenter;
private Location? transformCenter;
private Point viewCenter;
private double centerLongitude;
private double maxLatitude = 85.05112878; // default WebMercatorProjection
@ -326,12 +326,7 @@ namespace MapControl
private Location CoerceCenterProperty(Location center)
{
if (center == null)
{
center = new Location();
}
else if (
center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
if (center.Latitude < -maxLatitude || center.Latitude > maxLatitude ||
center.Longitude < -180d || center.Longitude > 180d)
{
center = new Location(
@ -391,7 +386,7 @@ namespace MapControl
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);
if (transformCenter != null)
if (transformCenter.HasValue)
{
var center = ViewToLocation(new Point(ActualWidth / 2d, ActualHeight / 2d));
var latitude = center.Latitude;
@ -419,7 +414,7 @@ namespace MapControl
{
// Check if transform center has moved across 180° longitude.
//
transformCenterChanged = Math.Abs(center.Longitude - transformCenter.Longitude) > 180d;
transformCenterChanged = Math.Abs(center.Longitude - transformCenter.Value.Longitude) > 180d;
ResetTransformCenter();
mapCenter = MapProjection.LocationToMap(center);
ViewTransform.SetTransform(mapCenter, viewCenter, viewScale, -Heading);

View file

@ -0,0 +1,54 @@
#if WPF
using System.Windows;
using System.Windows.Controls;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#elif AVALONIA
using Avalonia.Controls;
#endif
namespace MapControl
{
/// <summary>
/// ContentControl placed on a MapPanel at a geographic location specified by the Location property.
/// </summary>
public partial class MapContentControl : ContentControl
{
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.Register<MapContentControl, Location>(nameof(Location), default,
(control, oldValue, newValue) => MapPanel.SetLocation(control, newValue));
public static readonly DependencyProperty AutoCollapseProperty =
DependencyPropertyHelper.Register<MapContentControl, bool>(nameof(AutoCollapse), false,
(control, oldValue, newValue) => MapPanel.SetAutoCollapse(control, newValue));
/// <summary>
/// Gets/sets MapPanel.Location.
/// </summary>
public Location Location
{
get => (Location)GetValue(LocationProperty);
set => SetValue(LocationProperty, value);
}
/// <summary>
/// Gets/sets MapPanel.AutoCollapse.
/// </summary>
public bool AutoCollapse
{
get => (bool)GetValue(AutoCollapseProperty);
set => SetValue(AutoCollapseProperty, value);
}
}
/// <summary>
/// MapContentControl with a Pushpin Style.
/// </summary>
public partial class Pushpin : MapContentControl
{
}
}

View file

@ -1,10 +1,13 @@
#if WPF
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
#elif AVALONIA
@ -19,14 +22,17 @@ namespace MapControl
/// </summary>
public partial class MapItem : ListBoxItem, IMapElement
{
/// <summary>
/// Gets/sets MapPanel.AutoCollapse.
/// </summary>
public bool AutoCollapse
{
get => (bool)GetValue(AutoCollapseProperty);
set => SetValue(AutoCollapseProperty, value);
}
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.Register<MapItem, Location>(nameof(Location), default,
(item, oldValue, newValue) =>
{
MapPanel.SetLocation(item, newValue);
item.UpdateMapTransform();
});
public static readonly DependencyProperty AutoCollapseProperty =
DependencyPropertyHelper.Register<MapItem, bool>(nameof(AutoCollapse), false,
(item, oldValue, newValue) => MapPanel.SetAutoCollapse(item, newValue));
/// <summary>
/// Gets/sets MapPanel.Location.
@ -37,6 +43,15 @@ namespace MapControl
set => SetValue(LocationProperty, value);
}
/// <summary>
/// Gets/sets MapPanel.AutoCollapse.
/// </summary>
public bool AutoCollapse
{
get => (bool)GetValue(AutoCollapseProperty);
set => SetValue(AutoCollapseProperty, value);
}
/// <summary>
/// Implements IMapElement.ParentMap.
/// </summary>
@ -94,7 +109,7 @@ namespace MapControl
private void UpdateMapTransform()
{
if (MapTransform != null && ParentMap != null && Location != null)
if (MapTransform != null && ParentMap != null)
{
MapTransform.Matrix = ParentMap.GetMapToViewTransform(Location);
}

View file

@ -68,7 +68,7 @@ namespace MapControl
{
var location = MapPanel.GetLocation(ContainerFromItem(item));
return location != null && predicate(location);
return location.HasValue && predicate(location.Value);
});
}

View file

@ -99,15 +99,15 @@ namespace MapControl
/// <summary>
/// Gets the Location of an element.
/// </summary>
public static Location GetLocation(FrameworkElement element)
public static Location? GetLocation(FrameworkElement element)
{
return (Location)element.GetValue(LocationProperty);
return (Location?)element.GetValue(LocationProperty);
}
/// <summary>
/// Sets the Location of an element.
/// </summary>
public static void SetLocation(FrameworkElement element, Location value)
public static void SetLocation(FrameworkElement element, Location? value)
{
element.SetValue(LocationProperty, value);
}
@ -261,9 +261,9 @@ namespace MapControl
{
var location = GetLocation(element);
if (location != null)
if (location.HasValue)
{
var position = SetViewPosition(element, GetViewPosition(location));
var position = SetViewPosition(element, GetViewPosition(location.Value));
if (GetAutoCollapse(element))
{

View file

@ -17,15 +17,15 @@ namespace MapControl
public partial class MapPath : IMapElement
{
public static readonly DependencyProperty LocationProperty =
DependencyPropertyHelper.Register<MapPath, Location>(nameof(Location), null,
DependencyPropertyHelper.Register<MapPath, Location>(nameof(Location), default,
(path, oldValue, newValue) => path.UpdateData());
/// <summary>
/// Gets or sets a Location that is used as
/// - either the origin point of a geometry specified in projected map coordinates (meters)
/// - or as an optional anchor point to constrain the view position of MapPaths with multiple
/// Locations (like MapPolyline or MapPolygon) to the visible map viewport, as done
/// for elements where the MapPanel.Location property is set.
/// Gets or sets a Location that is either used as
/// - the origin point of a geometry specified in projected map coordinates (meters) or
/// - as an optional anchor point to constrain the view position of MapPaths with
/// multiple Locations (like MapPolyline or MapPolygon) to the visible map viewport,
/// as done for elements where the MapPanel.Location property is set.
/// </summary>
public Location Location
{
@ -64,7 +64,7 @@ namespace MapControl
protected virtual void UpdateData()
{
if (ParentMap != null && Location != null && Data != null)
if (ParentMap != null && Data != null)
{
SetDataTransform(ParentMap.GetMapToViewTransform(Location));
}
@ -72,23 +72,6 @@ namespace MapControl
MapPanel.SetLocation(this, Location);
}
protected double GetLongitudeOffset(Location location)
{
var longitudeOffset = 0d;
if (location != null != ParentMap.MapProjection.IsNormalCylindrical)
{
var position = ParentMap.LocationToView(location);
if (!ParentMap.InsideViewBounds(position))
{
longitudeOffset = ParentMap.NearestLongitude(location.Longitude) - location.Longitude;
}
}
return longitudeOffset;
}
protected Point LocationToMap(Location location, double longitudeOffset)
{
var point = ParentMap.MapProjection.LocationToMap(location.Latitude, location.Longitude + longitudeOffset);

View file

@ -1,5 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
#if WPF
using System.Windows;
using System.Windows.Media;
@ -37,6 +40,7 @@ namespace MapControl
protected MapPolypoint()
{
Data = new PolypointGeometry();
Location = new Location(0d, double.NaN);
}
protected void DataCollectionPropertyChanged(IEnumerable oldValue, IEnumerable newValue)
@ -58,5 +62,35 @@ namespace MapControl
{
UpdateData();
}
protected double GetLongitudeOffset(IEnumerable<Location> locations)
{
if (!ParentMap.MapProjection.IsNormalCylindrical)
{
return 0d;
}
Location location;
if (!double.IsNaN(Location.Longitude))
{
location = Location;
}
else if (locations != null && locations.Any())
{
location = locations.First();
}
else
{
return 0d;
}
if (ParentMap.InsideViewBounds(ParentMap.LocationToView(location)))
{
return 0d;
}
return ParentMap.NearestLongitude(location.Longitude) - location.Longitude;
}
}
}