using System.Globalization;
using System.Text.Json;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Maps;
using Microsoft.Maui.Maps.Handlers;
using Microsoft.Maui.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.Devices.Geolocation;
using IMap = Microsoft.Maui.Maps.IMap;
namespace CommunityToolkit.Maui.Maps.Handlers;
///
public partial class MapHandlerWindows : MapHandler
{
MapSpan? regionToGo;
///
/// Initializes a new instance of the class.
///
public MapHandlerWindows() : base(Mapper, CommandMapper)
{
Mapper.ModifyMapping(nameof(IMap.MapType), (handler, map, _) => MapMapType(handler, map));
Mapper.ModifyMapping(nameof(IMap.IsShowingUser), async (handler, map, _) => await MapIsShowingUser(handler, map));
Mapper.ModifyMapping(nameof(IMap.IsScrollEnabled), (handler, map, _) => MapIsScrollEnabled(handler, map));
Mapper.ModifyMapping(nameof(IMap.IsTrafficEnabled), (handler, map, _) => MapIsTrafficEnabled(handler, map));
Mapper.ModifyMapping(nameof(IMap.IsZoomEnabled), (handler, map, _) => MapIsZoomEnabled(handler, map));
Mapper.ModifyMapping(nameof(IMap.Pins), async (handler, map, _) => await MapPins(handler, map));
Mapper.ModifyMapping(nameof(IMap.Elements), (handler, map, _) => MapElements(handler, map));
CommandMapper.ModifyMapping(nameof(IMap.MoveToRegion), async (handler, map, args, _) => await MapMoveToRegion(handler, map, args));
}
internal static string? MapsKey { get; set; }
internal static string? MapPage { get; private set; }
///
protected override FrameworkElement CreatePlatformView()
{
if (string.IsNullOrEmpty(MapsKey))
{
throw new InvalidOperationException("You need to specify a Bing Maps Key");
}
MapPage = GetMapHtmlPage(MapsKey);
var webView = new MauiWebView(new WebViewHandler());
return webView;
}
///
protected override void ConnectHandler(FrameworkElement platformView)
{
if (platformView is MauiWebView mauiWebView)
{
LoadMap(platformView);
mauiWebView.NavigationCompleted += HandleWebViewNavigationCompleted;
mauiWebView.WebMessageReceived += WebViewWebMessageReceived;
Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged;
}
base.ConnectHandler(platformView);
}
///
protected override void DisconnectHandler(FrameworkElement platformView)
{
if (platformView is MauiWebView mauiWebView)
{
Connectivity.ConnectivityChanged -= Connectivity_ConnectivityChanged;
mauiWebView.NavigationCompleted -= HandleWebViewNavigationCompleted;
mauiWebView.WebMessageReceived -= WebViewWebMessageReceived;
}
base.DisconnectHandler(platformView);
}
///
/// Maps Map type
///
public static new Task MapMapType(IMapHandler handler, IMap map)
{
return TryCallJSMethod(handler.PlatformView, $"setMapType('{map.MapType}');");
}
///
/// Maps IsZoomEnabled
///
public static new Task MapIsZoomEnabled(IMapHandler handler, IMap map)
{
return TryCallJSMethod(handler.PlatformView, $"disableMapZoom({(!map.IsZoomEnabled).ToString().ToLower()});");
}
///
/// Maps IsScrollEnabled
///
public static new Task MapIsScrollEnabled(IMapHandler handler, IMap map)
{
return TryCallJSMethod(handler.PlatformView, $"disablePanning({(!map.IsScrollEnabled).ToString().ToLower()});");
}
///
/// Maps IsTrafficEnabled
///
public static new Task MapIsTrafficEnabled(IMapHandler handler, IMap map)
{
return TryCallJSMethod(handler.PlatformView, $"disableTraffic({(!map.IsTrafficEnabled).ToString().ToLower()});");
}
///
/// Maps IsShowingUser
///
public static new async Task MapIsShowingUser(IMapHandler handler, IMap map)
{
if (map.IsShowingUser)
{
var location = await GetCurrentLocation();
if (location is not null)
{
await TryCallJSMethod(handler.PlatformView, $"addLocationPin({location.Latitude.ToString(CultureInfo.InvariantCulture)},{location.Longitude.ToString(CultureInfo.InvariantCulture)});");
}
}
else
{
await TryCallJSMethod(handler.PlatformView, "removeLocationPin();");
}
}
///
/// Map Pins
///
public static new async Task MapPins(IMapHandler handler, IMap map)
{
await TryCallJSMethod(handler.PlatformView, "removeAllPins();");
var addPinTaskList = new List();
foreach (var pin in map.Pins)
{
addPinTaskList.Add(TryCallJSMethod(handler.PlatformView, $"addPin({pin.Location.Latitude.ToString(CultureInfo.InvariantCulture)}," +
$"{pin.Location.Longitude.ToString(CultureInfo.InvariantCulture)},'{pin.Label}', '{pin.Address}', '{(pin as Pin)?.Id}');"));
}
await Task.WhenAll(addPinTaskList);
}
///
/// Map Elements
///
public static new void MapElements(IMapHandler handler, IMap map) { }
///
/// Maps MoveToRegion
///
public static new async Task MapMoveToRegion(IMapHandler handler, IMap map, object? arg)
{
if (arg is not MapSpan newRegion)
{
return;
}
if (handler is MapHandlerWindows mapHandler)
{
mapHandler.regionToGo = newRegion;
}
await TryCallJSMethod(handler.PlatformView, $"setRegion({newRegion.Center.Latitude.ToString(CultureInfo.InvariantCulture)},{newRegion.Center.Longitude.ToString(CultureInfo.InvariantCulture)},{newRegion.LatitudeDegrees.ToString(CultureInfo.InvariantCulture)},{newRegion.LongitudeDegrees.ToString(CultureInfo.InvariantCulture)});");
}
static async Task TryCallJSMethod(FrameworkElement platformWebView, string script)
{
if (platformWebView is not WebView2 webView2)
{
return false;
}
await webView2.EnsureCoreWebView2Async();
var tcs = new TaskCompletionSource();
var isEnqueueSuccessful = webView2.DispatcherQueue.TryEnqueue(async () =>
{
await webView2.ExecuteScriptAsync(script);
tcs.SetResult();
});
if (!isEnqueueSuccessful)
{
return false;
}
await tcs.Task;
return true;
}
static string GetMapHtmlPage(string key)
{
var str = @$"
";
str += @"
";
return str;
}
static async Task GetCurrentLocation()
{
if (await Geolocator.RequestAccessAsync() != GeolocationAccessStatus.Allowed)
{
return null;
}
try
{
var geoLocator = new Geolocator();
var position = await geoLocator.GetGeopositionAsync();
return new Location(position.Coordinate.Latitude, position.Coordinate.Longitude);
}
catch
{
return null;
}
}
async void HandleWebViewNavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
// Update initial properties when our page is loaded
Mapper.UpdateProperties(this, VirtualView);
if (regionToGo is not null)
{
await MapMoveToRegion(this, VirtualView, regionToGo);
}
}
async void WebViewWebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
// For some reason the web message is empty
if (string.IsNullOrEmpty(args.WebMessageAsJson))
{
return;
}
var eventMessage = JsonSerializer.Deserialize(args.WebMessageAsJson, SerializerContext.Default.EventMessage);
// The web message (or it's ID) could not be deserialized to something we recognize
if (eventMessage is null || !Enum.TryParse(eventMessage.Id, true, out var eventId))
{
return;
}
var payloadAsString = eventMessage.Payload?.ToString();
// The web message does not have a payload
if (string.IsNullOrWhiteSpace(payloadAsString))
{
return;
}
switch (eventId)
{
case EventIdentifier.BoundsChanged:
var mapRect = JsonSerializer.Deserialize(payloadAsString, SerializerContext.Default.Bounds);
if (mapRect?.Center is not null)
{
VirtualView.VisibleRegion = new MapSpan(new Location(mapRect.Center.Latitude, mapRect.Center.Longitude),
mapRect.Height, mapRect.Width);
}
break;
case EventIdentifier.MapClicked:
var clickedLocation = JsonSerializer.Deserialize(payloadAsString, SerializerContext.Default.Location);
if (clickedLocation is not null)
{
VirtualView.Clicked(clickedLocation);
}
break;
case EventIdentifier.InfoWindowClicked:
var clickedInfoWindowWebView = JsonSerializer.Deserialize(payloadAsString, SerializerContext.Default.InfoWindow);
var clickedInfoWindowWebViewId = clickedInfoWindowWebView?.InfoWindowMarkerId;
if (!string.IsNullOrEmpty(clickedInfoWindowWebViewId))
{
var clickedPin = VirtualView.Pins.SingleOrDefault(p => (p as Pin)?.Id.ToString().Equals(clickedInfoWindowWebViewId) ?? false);
var hideInfoWindow = clickedPin?.SendInfoWindowClick();
if (hideInfoWindow is not false)
{
await TryCallJSMethod(PlatformView, "hideInfoWindow();");
}
}
break;
case EventIdentifier.PinClicked:
var clickedPinWebView = JsonSerializer.Deserialize(payloadAsString, SerializerContext.Default.Pin);
var clickedPinWebViewId = clickedPinWebView?.MarkerId?.ToString();
if (!string.IsNullOrEmpty(clickedPinWebViewId))
{
var clickedPin = VirtualView.Pins.SingleOrDefault(p => (p as Pin)?.Id.ToString().Equals(clickedPinWebViewId) ?? false);
var hideInfoWindow = clickedPin?.SendMarkerClick();
if (hideInfoWindow is not false)
{
await TryCallJSMethod(PlatformView, "hideInfoWindow();");
}
}
break;
}
}
void Connectivity_ConnectivityChanged(object? sender, ConnectivityChangedEventArgs e)
{
LoadMap(PlatformView);
}
static void LoadMap(FrameworkElement platformView)
{
if (platformView is MauiWebView mauiWebView && Connectivity.NetworkAccess == NetworkAccess.Internet)
{
mauiWebView.LoadHtml(MapPage, null);
}
}
}