dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.21k stars 1.74k forks source link

Provide a way to do a best fit based on a list of map pins #10718

Open smitha-cgi opened 2 years ago

smitha-cgi commented 2 years ago

Description

I have a map with many pins spread across different parts of the country. I want to be able to programmatically zoom in on certain areas so that I can see all pins within a particular region, e.g. 'zoom to best fit all of the pins in Texas'. There does not seem to be any way to do this with the existing MapSpan class - the FromCenterAndRadius does not help as I don't know the center point of my pin list, nor do I know what the radius of all of the pins in the region is.

Public API Changes

List<Pin> pins = new();

<some code here to populate the list of pins however you want>

MapSpan span = MapSpan.FromPins(pins);
map.MoveToRegion(span);

Intended Use-Case

Simple way to zoom in to a particular group of pins on a map

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

smitha-cgi commented 1 year ago

If anyone that wants to have a go at creating a PR that will provide this functionality - this code seems to work. LocationRect adapted from here

namespace SomeNamespace
{
    public static class LatLonInfo
    {
        public const double AngleConvert = 180;

        public const double MinLatValue = -90;
        public const double MaxLatValue = 90;
        public const double MinLonValue = -180;
        public const double MaxLonValue = 180;

        public const double MaxWorldLength = 360;

        /// <summary>
        /// Ensures longitude is valid
        /// </summary>
        /// <param name="longitude"></param>
        /// <returns></returns>
        public static double NormaliseLongitude(double longitude)
        {
            if (longitude < MinLonValue || longitude > MaxLonValue)
            {
                return longitude - Math.Floor((longitude + MaxLonValue) / MaxWorldLength) * MaxWorldLength;
            }

            return longitude;
        }

        /// <summary>
        /// Converts latitude or longitude to degrees
        /// </summary>
        /// <param name="latlon"></param>
        /// <returns></returns>
        public static double ToDegrees(double latlon)
        {
            double degree = 0;

            if (latlon >= MinLonValue && latlon <= MaxLonValue)
            {
                degree = (latlon + AngleConvert + MaxWorldLength) % MaxWorldLength;
            }

            return degree;
        }        
    }

    public class LocationRect
    {
        private readonly Location _northLocation = new();
        private readonly Location _southLocation = new();
        private readonly Location _eastLocation = new();
        private readonly Location _westLocation = new();

        private double _halfHeight;
        private double _halfWidth;       

        public Location Centre { get; private set; } = new();

        public double East
        {
            get
            {
                if (_halfWidth != LatLonInfo.MaxLonValue)
                {
                    return LatLonInfo.NormaliseLongitude(Centre.Longitude + _halfWidth);
                }
                return LatLonInfo.MaxLonValue;
            }
            set
            {
                Initialise(North, South, value, West);
            }
        }

        public Location EastLocation
        {
            get
            {
                return _eastLocation;
            }
        }

        public double North
        {
            get
            {
                return Centre.Latitude + _halfHeight;
            }
            set
            {
                Initialise(value, South, East, West);
            }
        }

        public Location NorthLocation
        {
            get
            {
                return _northLocation;
            }
        }

        public double South
        {
            get
            {
                return Centre.Latitude - _halfHeight;
            }
            set
            {
                Initialise(North, value, East, West);
            }
        }

        public Location SouthLocation
        {
            get
            {
                return _southLocation;
            }
        }

        public double West
        {
            get
            {
                if (_halfWidth != LatLonInfo.MaxLonValue)
                {
                    return LatLonInfo.NormaliseLongitude(Centre.Longitude - _halfWidth);
                }

                return LatLonInfo.MinLonValue;
            }
            set
            {
                Initialise(North, South, East, value);
            }
        }

        public Location WestLocation
        {
            get
            {
                return _westLocation;
            }
        }

        /// <summary>
        /// Creates a rectangle from a MapSpan
        /// </summary>
        /// <param name="span"></param>
        public LocationRect(MapSpan span)
        {         
            Location topLeft = new(span.Center.Latitude + span.LatitudeDegrees / 2, 
                span.Center.Longitude - span.LongitudeDegrees / 2);

            Location bottomRight = new(span.Center.Latitude - span.LatitudeDegrees / 2, 
                span.Center.Longitude + span.LongitudeDegrees / 2);      

            Initialise(topLeft.Latitude, bottomRight.Latitude, bottomRight.Longitude, topLeft.Longitude);
        }

        /// <summary>
        /// Creates a rectangle from a list of locations
        /// </summary>
        /// <param name="locations"></param>
        public LocationRect(List<Location> locations)
        {
            double north = LatLonInfo.MinLatValue;
            double south = LatLonInfo.MaxLatValue;
            double west = LatLonInfo.MaxLonValue;
            double east = LatLonInfo.MinLonValue;

            foreach (Location loc in locations)
            {
                north = Math.Max(north, loc.Latitude);
                south = Math.Min(south, loc.Latitude);
                west = Math.Min(west, loc.Longitude);
                east = Math.Max(east, loc.Longitude);
            }

            Initialise(north, south, east, west);
        }        

        /// <summary>
        /// Returns true if the coordinates are within the current bounds, otherwise false
        /// </summary>
        /// <param name="latitude"></param>
        /// <param name="longitude"></param>
        /// <returns></returns>
        public bool Contains(double latitude, double longitude)
        {
            return (West <= longitude && East >= longitude
                && South <= latitude && North >= latitude);
        }

        /// <summary>
        /// Initialises rectangle using NSEW
        /// </summary>
        /// <param name="north"></param>
        /// <param name="south"></param>
        /// <param name="east"></param>
        /// <param name="west"></param>
        private void Initialise(double north, double south, double east, double west)
        {
            if (west > east)
            {
                east += LatLonInfo.MaxWorldLength;
            }

            Centre.Latitude = (south + north) / 2.0;
            Centre.Longitude = LatLonInfo.NormaliseLongitude((west + east) / 2.0);            

            _halfHeight = (north - south) / 2.0;
            _halfWidth = Math.Abs(east - west) / 2.0;

            _northLocation.Latitude = North;
            _northLocation.Longitude = Centre.Longitude;

            _southLocation.Latitude = South;
            _southLocation.Longitude = Centre.Longitude;

            _eastLocation.Latitude = Centre.Latitude;
            _eastLocation.Longitude = East;

            _westLocation.Latitude = Centre.Latitude;
            _westLocation.Longitude = West;            
        }        
    }
}
LocationRect rect = new(locations);
if (rect.Centre != null && !double.IsNaN(rect.Centre.Latitude) && !double.IsNaN(rect.Centre.Longitude))
{
    double north = LatLonInfo.ToDegrees(rect.NorthLocation.Latitude);
    double south = LatLonInfo.ToDegrees(rect.SouthLocation.Latitude);
    double east = LatLonInfo.ToDegrees(rect.EastLocation.Longitude);
    double west = LatLonInfo.ToDegrees(rect.WestLocation.Longitude);

    double latDeg = Math.Abs(north - south);
    double lonDeg = Math.Abs(east - west);

    Map Span span = new(rect.Centre, latDeg, lonDeg);
        map.MoveToRegion(span);
}
ederbond commented 1 year ago

Based on @smitha-cgi 's solution I've came up with this extension method for IEnumerable one:

which you can you like this:

var span = locations.GetSpan();
MyMap.MoveToRegion(span);
public static class LocationExtensions
{
    public const double AngleConvert = 180;
    public const double MinLatValue = -90;
    public const double MaxLatValue = 90;
    public const double MinLonValue = -180;
    public const double MaxLonValue = 180;
    public const double MaxWorldLength = 360;

    public static MapSpan GetSpan(this List<Location> locations, double safeAreaLevel = 1.1)
    {
        var north = MinLatValue;
        var south = MaxLatValue;
        var west = MaxLonValue;
        var east = MinLonValue;

        foreach (var loc in locations)
        {
            north = Math.Max(north, loc.Latitude);
            south = Math.Min(south, loc.Latitude);
            west = Math.Min(west, loc.Longitude);
            east = Math.Max(east, loc.Longitude);
        }

        if (west > east)
        {
            east += MaxWorldLength;
        }

        var halfHeight = (north - south) / 2.0;
        var halfWidth = Math.Abs(east - west) / 2.0;
        var centre = new Location((south + north) / 2.0, NormalizeLongitude((west + east) / 2.0));
        var northLocation = new Location(centre.Latitude + halfHeight, centre.Longitude);
        var southLocation = new Location(centre.Latitude - halfHeight, centre.Longitude);
        var eastLocation = new Location(centre.Latitude, halfWidth != MaxLonValue ? NormalizeLongitude(centre.Longitude + halfWidth) : MaxLonValue);
        var westLocation = new Location(centre.Latitude, !halfWidth.Equals(MaxLonValue) ? NormalizeLongitude(centre.Longitude - halfWidth) : MinLonValue);

        north = ToDegrees(northLocation.Latitude);
        south = ToDegrees(southLocation.Latitude);
        east =ToDegrees(eastLocation.Longitude);
        west = ToDegrees(westLocation.Longitude);

        var latDeg = Math.Abs(north - south) * safeAreaLevel;
        var lonDeg = Math.Abs(east - west) * safeAreaLevel;

        MapSpan span = new(centre, latDeg, lonDeg);
        return span;
    }

    private static double NormalizeLongitude(double longitude)
    {
        if (longitude is < MinLonValue or > MaxLonValue)
        {
            return longitude - Math.Floor((longitude + MaxLonValue) / MaxWorldLength) * MaxWorldLength;
        }

        return longitude;
    }

    private static double ToDegrees(double coordinate)
    {
        double degree = 0;

        if (coordinate is >= MinLonValue and <= MaxLonValue)
        {
            degree = (coordinate + AngleConvert + MaxWorldLength) % MaxWorldLength;
        }

        return degree;
    }
}