File: Platform\iOS\MauiMKMapView.cs
Web Access
Project: src\src\Core\maps\src\Maps.csproj (Microsoft.Maui.Maps)
using System;
using System.Collections;
using System.Linq;
using CoreLocation;
using MapKit;
using Microsoft.Maui.Maps.Handlers;
using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Maps.Platform
{
	public class MauiMKMapView : MKMapView
	{
		WeakReference<IMapHandler> _handlerRef;
		object? _lastTouchedView;
		UITapGestureRecognizer? _mapClickedGestureRecognizer;
 
		public MauiMKMapView(IMapHandler handler)
		{
			_handlerRef = new WeakReference<IMapHandler>(handler);
			OverlayRenderer = GetViewForOverlayDelegate;
		}
 
		public override void MovedToWindow()
		{
			base.MovedToWindow();
			if (Window != null)
				Startup();
			else
				Cleanup();
		}
 
		protected virtual MKOverlayRenderer GetViewForOverlayDelegate(MKMapView mapview, IMKOverlay overlay)
		{
			MKOverlayRenderer? overlayRenderer = null;
			switch (overlay)
			{
				case MKPolyline polyline:
					overlayRenderer = GetMapElement<MKPolylineRenderer>(polyline);
					break;
				case MKPolygon polygon:
					overlayRenderer = GetMapElement<MKPolygonRenderer>(polygon);
					break;
				case MKCircle circle:
					overlayRenderer = GetMapElement<MKCircleRenderer>(circle);
					break;
				default:
					break;
			}
			if (overlayRenderer == null)
				throw new InvalidOperationException($"MKOverlayRenderer not found for {overlay}.");
 
			return overlayRenderer;
		}
 
#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
		MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
#pragma warning restore CS0108 // Member hides inherited member; missing new keyword
		{
			MKAnnotationView? mapPin;
 
			// https://bugzilla.xamarin.com/show_bug.cgi?id=26416
			var userLocationAnnotation = Runtime.GetNSObject(annotation.Handle) as MKUserLocation;
			if (userLocationAnnotation != null)
				return null!;
 
			const string defaultPinId = "defaultPin";
			mapPin = mapView.DequeueReusableAnnotation(defaultPinId);
			if (mapPin == null)
			{
				if (OperatingSystem.IsIOSVersionAtLeast(11))
				{
					mapPin = new MKMarkerAnnotationView(annotation, defaultPinId);
				}
				else
				{
					mapPin = new MKPinAnnotationView(annotation, defaultPinId);
 
				}
 
				mapPin.CanShowCallout = true;
 
				if (OperatingSystem.IsIOSVersionAtLeast(11))
				{
					// Need to set this to get the callout bubble to show up
					// Without this no callout is shown, it's displayed differently
					mapPin.RightCalloutAccessoryView = new UIView();
				}
			}
 
			mapPin.Annotation = annotation;
			AttachGestureToPin(mapPin, annotation);
 
			return mapPin;
		}
 
		internal void AddPins(IList pins)
		{
			_handlerRef.TryGetTarget(out IMapHandler? handler);
			if (handler?.MauiContext == null)
				return;
 
			if (Annotations?.Length > 0)
				RemoveAnnotations(Annotations);
 
			foreach (IMapPin pin in pins)
			{
				if (pin.ToHandler(handler.MauiContext).PlatformView is IMKAnnotation annotation)
				{
					pin.MarkerId = annotation;
					AddAnnotation(annotation);
				}
			}
		}
 
		internal void ClearMapElements()
		{
			var elements = Overlays;
 
			if (elements == null)
				return;
 
			foreach (IMKOverlay overlay in elements)
			{
				RemoveOverlay(overlay);
			}
		}
 
		internal void AddElements(IList elements)
		{
			foreach (IMapElement element in elements)
			{
				IMKOverlay? overlay = null;
				switch (element)
				{
					case IGeoPathMapElement geoPathElement:
						if (geoPathElement is IFilledMapElement)
							overlay = MKPolygon.FromCoordinates(geoPathElement
							.Select(position => new CLLocationCoordinate2D(position.Latitude, position.Longitude))
							.ToArray());
						else
							overlay = MKPolyline.FromCoordinates(geoPathElement
								.Select(position => new CLLocationCoordinate2D(position.Latitude, position.Longitude))
								.ToArray());
						break;
					case ICircleMapElement circleElement:
						overlay = MKCircle.Circle(
							new CLLocationCoordinate2D(circleElement.Center.Latitude, circleElement.Center.Longitude),
							circleElement.Radius.Meters);
						break;
				}
 
				if (overlay != null)
				{
					element.MapElementId = overlay;
					AddOverlay(overlay);
				}
			}
		}
 
		internal void RemoveElements(IList elements)
		{
			foreach (IMapElement element in elements)
			{
				if (element.MapElementId is IMKOverlay overlay)
					RemoveOverlay(overlay);
			}
		}
 
		void Startup()
		{
			RegionChanged += MkMapViewOnRegionChanged;
			DidSelectAnnotationView += MkMapViewOnAnnotationViewSelected;
 
			AddGestureRecognizer(_mapClickedGestureRecognizer = new UITapGestureRecognizer(OnMapClicked)
			{
				ShouldReceiveTouch = OnShouldReceiveMapTouch
			});
		}
 
		void Cleanup()
		{
			if (_mapClickedGestureRecognizer != null)
			{
				RemoveGestureRecognizer(_mapClickedGestureRecognizer);
				_mapClickedGestureRecognizer.Dispose();
				_mapClickedGestureRecognizer = null;
			}
			RegionChanged -= MkMapViewOnRegionChanged;
			DidSelectAnnotationView -= MkMapViewOnAnnotationViewSelected;
		}
 
		void MkMapViewOnAnnotationViewSelected(object? sender, MKAnnotationViewEventArgs e)
		{
			var annotation = e.View.Annotation;
			var pin = GetPinForAnnotation(annotation!);
 
			if (pin == null)
				return;
 
			// SendMarkerClick() returns the value of PinClickedEventArgs.HideInfoWindow
			// Hide the info window by deselecting the annotation
			bool deselect = pin.SendMarkerClick();
 
			if (deselect)
				DeselectAnnotation(annotation, false);
		}
 
		void MkMapViewOnRegionChanged(object? sender, MKMapViewChangeEventArgs e)
		{
			if (_handlerRef.TryGetTarget(out IMapHandler? handler) && handler?.VirtualView != null)
				handler.VirtualView.VisibleRegion = new MapSpan(new Devices.Sensors.Location(Region.Center.Latitude, Region.Center.Longitude), Region.Span.LatitudeDelta, Region.Span.LongitudeDelta);
		}
 
		IMapPin GetPinForAnnotation(IMKAnnotation annotation)
		{
			IMapPin targetPin = null!;
			_handlerRef.TryGetTarget(out IMapHandler? handler);
			IMap map = handler?.VirtualView!;
 
			for (int i = 0; i < map.Pins.Count; i++)
			{
				var pin = map.Pins[i];
				if ((pin?.MarkerId as IMKAnnotation) == annotation)
				{
					targetPin = pin;
					break;
				}
			}
 
			return targetPin;
		}
 
		void AttachGestureToPin(MKAnnotationView mapPin, IMKAnnotation annotation)
		{
			var recognizers = mapPin.GestureRecognizers;
 
			if (recognizers != null)
			{
				foreach (var r in recognizers)
				{
					mapPin.RemoveGestureRecognizer(r);
				}
			}
 
			var recognizer = new UITapGestureRecognizer(g => OnCalloutClicked(annotation))
			{
				ShouldReceiveTouch = (gestureRecognizer, touch) =>
				{
					_lastTouchedView = touch.View;
					return true;
				}
			};
 
			mapPin.AddGestureRecognizer(recognizer);
		}
 
		void OnCalloutClicked(IMKAnnotation annotation)
		{
			// lookup pin
			var targetPin = GetPinForAnnotation(annotation);
 
			// pin not found. Must have been activated outside of forms
			if (targetPin == null)
				return;
 
			// if the tap happened on the annotation view itself, skip because this is what happens when the callout is showing
			// when the callout is already visible the tap comes in on a different view
			if (_lastTouchedView is MKAnnotationView)
				return;
 
			targetPin.SendMarkerClick();
 
			// SendInfoWindowClick() returns the value of PinClickedEventArgs.HideInfoWindow
			// Hide the info window by deselecting the annotation
			bool deselect = targetPin.SendInfoWindowClick();
			if (deselect)
				DeselectAnnotation(annotation, true);
		}
 
		T? GetMapElement<T>(IMKOverlay mkPolyline) where T : MKOverlayRenderer
		{
			_handlerRef.TryGetTarget(out IMapHandler? handler);
			var map = handler?.VirtualView;
			IMapElement mapElement = default!;
			for (int i = 0; i < map?.Elements.Count; i++)
			{
				var element = map.Elements[i];
				if (ReferenceEquals(element.MapElementId, mkPolyline))
				{
					mapElement = element;
					break;
				}
			}
			//Make sure we Disconnect old handler we don't want to reuse that one
			mapElement?.Handler?.DisconnectHandler();
			return mapElement?.ToHandler(handler?.MauiContext!).PlatformView as T;
		}
 
		static bool OnShouldReceiveMapTouch(UIGestureRecognizer recognizer, UITouch touch)
		{
			if (touch.View is MKAnnotationView)
				return false;
 
			return true;
		}
 
		static void OnMapClicked(UITapGestureRecognizer recognizer)
		{
			if (recognizer.View is not MauiMKMapView mauiMkMapView)
				return;
 
			var tapPoint = recognizer.LocationInView(mauiMkMapView);
			var tapGPS = mauiMkMapView.ConvertPoint(tapPoint, mauiMkMapView);
 
			if (mauiMkMapView._handlerRef.TryGetTarget(out IMapHandler? handler))
				handler?.VirtualView.Clicked(new Devices.Sensors.Location(tapGPS.Latitude, tapGPS.Longitude));
		}
	}
}