File: Map.cs
Web Access
Project: src\src\Controls\Maps\src\Controls.Maps.csproj (Microsoft.Maui.Controls.Maps)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Maps;
 
namespace Microsoft.Maui.Controls.Maps
{
	/// <summary>
	/// The Map control is a cross-platform view for displaying and annotating maps.
	/// </summary>
	public partial class Map : View
	{
		/// <summary>Bindable property for <see cref="MapType"/>.</summary>
		public static readonly BindableProperty MapTypeProperty = BindableProperty.Create(nameof(MapType), typeof(MapType), typeof(Map), default(MapType));
 
		/// <summary>Bindable property for <see cref="IsShowingUser"/>.</summary>
		public static readonly BindableProperty IsShowingUserProperty = BindableProperty.Create(nameof(IsShowingUser), typeof(bool), typeof(Map), default(bool));
 
		/// <summary>Bindable property for <see cref="IsTrafficEnabled"/>.</summary>
		public static readonly BindableProperty IsTrafficEnabledProperty = BindableProperty.Create(nameof(IsTrafficEnabled), typeof(bool), typeof(Map), default(bool));
 
		/// <summary>Bindable property for <see cref="IsScrollEnabled"/>.</summary>
		public static readonly BindableProperty IsScrollEnabledProperty = BindableProperty.Create(nameof(IsScrollEnabled), typeof(bool), typeof(Map), true);
 
		/// <summary>Bindable property for <see cref="IsZoomEnabled"/>.</summary>
		public static readonly BindableProperty IsZoomEnabledProperty = BindableProperty.Create(nameof(IsZoomEnabled), typeof(bool), typeof(Map), true);
 
		/// <summary>Bindable property for <see cref="ItemsSource"/>.</summary>
		public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(Map), default(IEnumerable),
			propertyChanged: (b, o, n) => ((Map)b).OnItemsSourcePropertyChanged((IEnumerable)o, (IEnumerable)n));
 
		/// <summary>Bindable property for <see cref="ItemTemplate"/>.</summary>
		public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(Map), default(DataTemplate),
			propertyChanged: (b, o, n) => ((Map)b).OnItemTemplatePropertyChanged((DataTemplate)o, (DataTemplate)n));
 
		/// <summary>Bindable property for <see cref="ItemTemplateSelector"/>.</summary>
		public static readonly BindableProperty ItemTemplateSelectorProperty = BindableProperty.Create(nameof(ItemTemplateSelector), typeof(DataTemplateSelector), typeof(Map), default(DataTemplateSelector),
			propertyChanged: (b, o, n) => ((Map)b).OnItemTemplateSelectorPropertyChanged());
 
		readonly ObservableCollection<Pin> _pins = new();
		readonly ObservableCollection<MapElement> _mapElements = new();
		MapSpan? _visibleRegion;
		MapSpan? _lastMoveToRegion;
 
		/// <summary>
		/// Initializes a new instance of the <see cref="Map"/> class with a region.
		/// </summary>
		/// <param name="region">The region that should be initially shown by the map.</param>
		public Map(MapSpan region)
		{
			MoveToRegion(region);
#pragma warning disable CS0618 // Type or member is obsolete
			VerticalOptions = HorizontalOptions = LayoutOptions.FillAndExpand;
#pragma warning restore CS0618 // Type or member is obsolete
 
			_pins.CollectionChanged += PinsOnCollectionChanged;
			_mapElements.CollectionChanged += MapElementsCollectionChanged;
		}
 
		/// <summary>
		/// Initializes a new instance of the <see cref="Map"/> class with a region.
		/// </summary>
		// <remarks>The selected region will default to Maui, Hawaii.</remarks>
		public Map() : this(new MapSpan(new Devices.Sensors.Location(20.793062527, -156.336394697), 0.5, 0.5))
		{
		}
 
		/// <summary>
		/// Gets or sets a value that indicates if scrolling by user input is enabled. Default value is <see langword="true"/>.
		/// This is a bindable property.
		/// </summary>
		public bool IsScrollEnabled
		{
			get { return (bool)GetValue(IsScrollEnabledProperty); }
			set { SetValue(IsScrollEnabledProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets a value that indicates if zooming by user input is enabled. Default value is <see langword="true"/>.
		/// This is a bindable property.
		/// </summary>
		public bool IsZoomEnabled
		{
			get { return (bool)GetValue(IsZoomEnabledProperty); }
			set { SetValue(IsZoomEnabledProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets a value that indicates if the map shows an indicator of the current position of this device. Default value is <see langword="false"/>
		/// This is a bindable property.
		/// </summary>
		/// <remarks>Depending on the platform it is likely that runtime permission(s) need to be requested to determine the current location of the device.</remarks>
		public bool IsShowingUser
		{
			get { return (bool)GetValue(IsShowingUserProperty); }
			set { SetValue(IsShowingUserProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets a value that indicates if the map shows current traffic information. Default value is <see langword="false"/>.
		/// This is a bindable property.
		/// </summary>
		public bool IsTrafficEnabled
		{
			get => (bool)GetValue(IsTrafficEnabledProperty);
			set => SetValue(IsTrafficEnabledProperty, value);
		}
 
		/// <summary>
		/// Gets or sets the style of the map. Default value is <see cref="MapType.Street"/>. 
		/// This is a bindable property.
		/// </summary>
		public MapType MapType
		{
			get { return (MapType)GetValue(MapTypeProperty); }
			set { SetValue(MapTypeProperty, value); }
		}
 
		/// <summary>
		/// Gets the pins currently added to this map.
		/// </summary>
		public IList<Pin> Pins
		{
			get { return _pins; }
		}
 
		/// <summary>
		/// Gets or sets the object that represents the collection of pins that should be shown on the map.
		/// This is a bindable property.
		/// </summary>
		public IEnumerable ItemsSource
		{
			get { return (IEnumerable)GetValue(ItemsSourceProperty); }
			set { SetValue(ItemsSourceProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets the template that is to be applied to each object in <see cref="ItemsSource"/>.
		/// This is a bindable property.
		/// </summary>
		public DataTemplate ItemTemplate
		{
			get { return (DataTemplate)GetValue(ItemTemplateProperty); }
			set { SetValue(ItemTemplateProperty, value); }
		}
 
		/// <summary>
		/// Gets or sets the object that selects the template that is to be applied to each object in <see cref="ItemsSource"/>.
		/// This is a bindable property.
		/// </summary>
		public DataTemplateSelector ItemTemplateSelector
		{
			get { return (DataTemplateSelector)GetValue(ItemTemplateSelectorProperty); }
			set { SetValue(ItemTemplateSelectorProperty, value); }
		}
 
		/// <summary>
		/// Gets the elements (pins, polygons, polylines, etc.) currently added to this map.
		/// </summary>
		public IList<MapElement> MapElements => _mapElements;
 
		/// <summary>
		/// Occurs when the user clicks/taps on the map control.
		/// </summary>
		public event EventHandler<MapClickedEventArgs>? MapClicked;
 
		/// <summary>
		/// Gets the currently visible region of the map.
		/// </summary>
		public MapSpan? VisibleRegion
		{
			get { return _visibleRegion; }
		}
 
		/// <summary>
		/// Returns an enumarator of all the pins that are currently added to the map.
		/// </summary>
		/// <returns>An instance of <see cref="IEnumerator{IMapPin}"/>.</returns>
		public IEnumerator<IMapPin> GetEnumerator()
		{
			return _pins.GetEnumerator();
		}
 
		/// <summary>
		/// Adjusts the viewport of the map control to view the specified region.
		/// </summary>
		/// <param name="mapSpan">A <see cref="MapSpan"/> object containing details on what region should be shown.</param>
		/// <exception cref="ArgumentNullException">Thrown when <paramref name="mapSpan"/> is <see langword="null"/>.</exception>
		public void MoveToRegion(MapSpan mapSpan)
		{
			if (mapSpan is null)
			{
				throw new ArgumentNullException(nameof(mapSpan));
			}
 
			_lastMoveToRegion = mapSpan;
			Handler?.Invoke(nameof(IMap.MoveToRegion), _lastMoveToRegion);
		}
 
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}
 
		void SetVisibleRegion(MapSpan? visibleRegion)
		{
			if (visibleRegion is null)
			{
				throw new ArgumentNullException(nameof(visibleRegion));
			}
 
			if (_visibleRegion == visibleRegion)
			{
				return;
			}
 
			OnPropertyChanging(nameof(VisibleRegion));
			_visibleRegion = visibleRegion;
			OnPropertyChanged(nameof(VisibleRegion));
		}
 
		void PinsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
		{
			if (e.NewItems is not null && e.NewItems.Cast<Pin>().Any(pin => pin.Label is null))
			{
				throw new ArgumentException("Pin must have a Label to be added to a map");
			}
 
			Handler?.UpdateValue(nameof(IMap.Pins));
		}
 
		void MapElementsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
		{
			Handler?.UpdateValue(nameof(IMap.Elements));
			if (e.NewItems is not null)
			{
				foreach (MapElement item in e.NewItems)
				{
					item.PropertyChanged += MapElementPropertyChanged;
				}
			}
 
			if (e.OldItems is not null)
			{
				foreach (MapElement item in e.OldItems)
				{
					item.PropertyChanged -= MapElementPropertyChanged;
				}
			}
		}
 
		void MapElementPropertyChanged(object? sender, PropertyChangedEventArgs e)
		{
			if (sender is MapElement mapElement)
			{
				var index = MapElements.IndexOf(mapElement);
				var args = new Maui.Maps.Handlers.MapElementHandlerUpdate(index, mapElement);
				Handler?.Invoke(nameof(Maui.Maps.Handlers.IMapHandler.UpdateMapElement), args);
			}
		}
 
		void OnItemsSourcePropertyChanged(IEnumerable oldItemsSource, IEnumerable newItemsSource)
		{
			if (oldItemsSource is INotifyCollectionChanged ncc)
			{
				ncc.CollectionChanged -= OnItemsSourceCollectionChanged;
			}
 
			if (newItemsSource is INotifyCollectionChanged ncc1)
			{
				ncc1.CollectionChanged += OnItemsSourceCollectionChanged;
			}
 
			_pins.Clear();
			CreatePinItems();
		}
 
		void OnItemTemplatePropertyChanged(DataTemplate oldItemTemplate, DataTemplate newItemTemplate)
		{
			if (newItemTemplate is DataTemplateSelector)
			{
				throw new NotSupportedException(
					$"The {nameof(Map)}.{ItemTemplateProperty.PropertyName} property only supports {nameof(DataTemplate)}." +
					$" Set the {nameof(Map)}.{ItemTemplateSelectorProperty.PropertyName} property instead to use a {nameof(DataTemplateSelector)}");
			}
 
			_pins.Clear();
			CreatePinItems();
		}
 
		void OnItemTemplateSelectorPropertyChanged()
		{
			_pins.Clear();
			CreatePinItems();
		}
 
		void OnItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
		{
			e.Apply(
				insert: (item, _, __) => CreatePin(item),
				removeAt: (item, _) => RemovePin(item),
				reset: () => _pins.Clear());
			Handler?.UpdateValue(nameof(IMap.Pins));
		}
 
		void CreatePinItems()
		{
			if (ItemsSource is null || (ItemTemplate is null && ItemTemplateSelector is null))
			{
				return;
			}
 
			foreach (object item in ItemsSource)
			{
				CreatePin(item);
			}
 
			Handler?.UpdateValue(nameof(IMap.Pins));
		}
 
		void CreatePin(object newItem)
		{
			DataTemplate? itemTemplate = ItemTemplate;
			if (itemTemplate is null)
			{
				itemTemplate = ItemTemplateSelector?.SelectTemplate(newItem, this);
			}
 
			if (itemTemplate is null)
			{
				return;
			}
 
			var pin = (Pin)itemTemplate.CreateContent();
			pin.BindingContext = newItem;
			_pins.Add(pin);
		}
 
		void RemovePin(object itemToRemove)
		{
			//// Instead of just removing by item (i.e. _pins.Remove(pinToRemove))
			////  we need to remove by index because of how Pin.Equals() works
			for (int i = 0; i < _pins.Count; ++i)
			{
				Pin? pin = _pins[i] as Pin;
				if (pin is not null)
				{
					if (pin.BindingContext?.Equals(itemToRemove) == true)
					{
						_pins.RemoveAt(i);
					}
				}
			}
		}
	}
}