-
Notifications
You must be signed in to change notification settings - Fork 390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Maps] Add Windows map handler based on Webview #604
Conversation
Thanks Rui! Could you first please submit a New Feature Proposal. Once you've submitted a Proposal, we will vote to approve adding the new feature to the .NET MAUI Community Toolkit. Here's some more information on the process: |
Closing this PR until Proposal has been approved |
The proposal has been approved 🥳 |
src/CommunityToolkit.Maui.Maps/CommunityToolkit.Maui.Maps.csproj
Outdated
Show resolved
Hide resolved
src/CommunityToolkit.Maui.Maps/Handler/Map/MapHandler.Windows.cs
Outdated
Show resolved
Hide resolved
c27c46d
to
4c28551
Compare
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of things to get this all done, great work Vlad, thank you!
samples/CommunityToolkit.Maui.Sample/Pages/Views/Maps/MapsPinsPage.xaml.cs
Outdated
Show resolved
Hide resolved
samples/CommunityToolkit.Maui.Sample/Pages/Views/Maps/BasicMapsPage.xaml
Outdated
Show resolved
Hide resolved
samples/CommunityToolkit.Maui.Sample/Pages/Views/Maps/BasicMapsPage.xaml.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! Looks good to me!
I'm not sure if this is the appropriate place to ask. But could someone help out with a proof of concept on a custom handler to use custom icons for pins on Windows? There are similar guides for iOS and Android out there, but nothing for this Windows implementation. |
I built one that expects SVG Path data (which is the same as MAUI Path data) I consume it with an MVVM approach, as explained here: https://dev.to/symbiogenesis/use-net-maui-map-control-with-mvvm-dfl If you build on top of the GitHub repo linked at the bottom of my blog post, it might be easiest. Because everything is wired up already. Overwrite the Windows map handler in that repo with the following code. I didn't put in any work to make it a fully general library, so you will just have to work with the build errors. It has a bunch of features you may not need, like pin selection, pin cloning, and pin dragging. It is expecting a type called Record with a You could rewrite it to use bitmaps instead, if you like. But simple single-path SVG files are nice because vectors. // <copyright file="CustomMapHandler.cs" company="Edward Miller">
// The MIT License (MIT)
// Copyright (c) Edward Miller
// </copyright>
/* -----------------------------------------------------------------------
Original CommunityToolkit.Maui code, from which this was adapted, was under the the following license:
The MIT License (MIT)
Copyright (c) .NET Foundation and Contributors
All Rights Reserved
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ----------------------------------------------------------------------- */
namespace MyProject.Platforms.Windows.Handlers;
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text.Json;
using MyProject;
using MyProject.Converters;
using MyProject.CustomControls;
using MyProject.CustomControls.Interfaces;
using MyProject.Data.Enums;
using MyProject.Data.Models;
using MyProject.Extensions;
using MyProject.Messages;
using MyProject.Messages.MessageArgs;
using CommunityToolkit.Mvvm.Messaging;
using global::Windows.Devices.Geolocation;
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 NUlid;
using IMap = Microsoft.Maui.Maps.IMap;
[SupportedOSPlatform("windows10.0.19041.0")]
internal sealed class CustomMapHandler : MapHandler, IRecipient<RecordTypeChanged>
{
public static readonly PropertyMapper<ICustomMap, IMapHandler> CustomMapper = new(Mapper)
{
[nameof(ICustomMap.MapState)] = MapMapState,
[nameof(ICustomMap.MapType)] = MapMapType,
[nameof(ICustomMap.IsShowingUser)] = MapIsShowingUser,
[nameof(ICustomMap.IsScrollEnabled)] = MapIsScrollEnabled,
[nameof(ICustomMap.IsTrafficEnabled)] = MapIsTrafficEnabled,
[nameof(ICustomMap.IsZoomEnabled)] = MapIsZoomEnabled,
[nameof(ICustomMap.Pins)] = MapPins,
[nameof(ICustomMap.Elements)] = MapElements,
[nameof(ICustomMap.SelectedItem)] = MapSelectedItem,
};
public static readonly CommandMapper<ICustomMap, IMapHandler> CustomCommandMapper = new(CommandMapper)
{
[nameof(ICustomMap.MoveToRegion)] = MapMoveToRegion,
};
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly IMessenger messenger;
private readonly MauiWebView mauiWebView;
private MapSpan? regionToGo;
private bool isWebViewLoaded;
public CustomMapHandler(IMessenger messenger, MauiWebView mauiWebView)
: base(CustomMapper, CustomCommandMapper)
{
this.messenger = messenger;
this.mauiWebView = mauiWebView;
if (string.IsNullOrWhiteSpace(MapsKey))
{
throw new InvalidOperationException("You need to specify a Bing Maps Key");
}
var symbolSize = RecordToPathConverter.CalculatedSize / 2;
var mapPage = GetMapHtmlPage(MapsKey, !EnableStreetView, EnableMapSymbols, symbolSize);
this.mauiWebView.LoadHtml(mapPage, null);
this.messenger.Register(this);
}
~CustomMapHandler()
{
this.messenger.UnregisterAll(this);
}
public string? LastPinsJson { get; set; }
internal static string? MapsKey { get; set; }
internal static bool EnableStreetView { get; set; }
internal static bool EnableMapSymbols { get; set; } = true;
public static void MapMapState(IMapHandler handler, IMap map)
{
if (map is not CustomMap customMap)
{
return;
}
var isDragging = customMap.MapState is MapState.Moving or MapState.Cloning;
if (!isDragging)
{
return;
}
if (customMap.SelectedItem is not Record selectedRecord)
{
return;
}
var isCloning = customMap.MapState is MapState.Cloning;
CallJSMethod(handler, $"beginDragPin('{selectedRecord.IdString}', {isCloning.ToStringLower()});");
}
public static new void MapMapType(IMapHandler handler, IMap map) => CallJSMethod(handler, $"setMapType('{map.MapType}');");
public static new void MapIsZoomEnabled(IMapHandler handler, IMap map) => CallJSMethod(handler, $"toggleMapZoom({map.IsZoomEnabled.ToStringLower()});");
public static new void MapIsScrollEnabled(IMapHandler handler, IMap map) => CallJSMethod(handler, $"togglePanning({map.IsScrollEnabled.ToStringLower()});");
public static new void MapIsTrafficEnabled(IMapHandler handler, IMap map) => CallJSMethod(handler, $"toggleTraffic({map.IsTrafficEnabled.ToStringLower()});");
public static new async void MapIsShowingUser(IMapHandler handler, IMap map)
{
if (map.IsShowingUser)
{
var location = await GetCurrentLocation();
if (location != null)
{
CallJSMethod(handler, $"setLocationPin({location.Latitude},{location.Longitude});");
}
}
else
{
CallJSMethod(handler, "removeLocationPin();");
}
}
public static new void MapPins(IMapHandler handler, IMap map)
{
if (map is not CustomMap customMap || !customMap.IsLoaded)
{
return;
}
var customPins = customMap.ItemsSource.Cast<Record>().Select(GeneratePinDTO).Where(p => p.Location != null).ToList();
var pinsJson = JsonSerializer.Serialize(customPins, JsonOptions);
if (handler is CustomMapHandler customMapHandler && pinsJson != customMapHandler.LastPinsJson)
{
customMapHandler.LastPinsJson = pinsJson;
CallJSMethod(handler, $"addPins({pinsJson});");
}
}
public static void MapSelectedItem(IMapHandler handler, ICustomMap map)
{
if (handler is not CustomMapHandler customMapHandler)
{
return;
}
if (map.SelectedViaJavascript)
{
map.SelectedViaJavascript = false;
return;
}
if (map.SelectedItem is not Record selectedRecord || selectedRecord.Location is null)
{
CallJSMethod(customMapHandler, $"unsetSelectedPin();");
return;
}
var selectedLocation = LocationExtensions.GetLocation(selectedRecord.Location);
handler.VirtualView.Clicked(selectedLocation);
}
public static new void MapElements(IMapHandler handler, IMap map) => Debug.WriteLine("Update for all Map Elements requested.");
public static new void UpdateMapElement(IMapElement element) => Debug.WriteLine($"Update for Map Element requested for ID: {element.MapElementId}");
public static new void MapMoveToRegion(IMapHandler handler, IMap map, object? arg)
{
if (arg is not MapSpan newRegion || handler is not CustomMapHandler mapHandler)
{
return;
}
mapHandler.regionToGo = newRegion;
var (southWest, northEast) = newRegion.ToBounds();
CallJSMethod(handler, $"setRegion({southWest.Latitude}, {southWest.Longitude}, {northEast.Latitude}, {northEast.Longitude});");
}
public void Receive(RecordTypeChanged message)
{
var recordTypeChangedJson = JsonSerializer.Serialize(message.Value, JsonOptions);
CallJSMethod(this, $"changeRecordType({recordTypeChangedJson});");
}
/// <inheritdoc/>
protected override FrameworkElement CreatePlatformView() => this.mauiWebView;
protected override void ConnectHandler(FrameworkElement platformView)
{
base.ConnectHandler(platformView);
if (platformView is MauiWebView mauiWebView)
{
mauiWebView.NavigationCompleted += this.WebViewNavigationCompleted;
mauiWebView.WebMessageReceived += this.WebViewWebMessageReceived;
}
}
protected override void DisconnectHandler(FrameworkElement platformView)
{
if (platformView is MauiWebView mauiWebView)
{
mauiWebView.NavigationCompleted -= this.WebViewNavigationCompleted;
mauiWebView.WebMessageReceived -= this.WebViewWebMessageReceived;
}
base.DisconnectHandler(platformView);
}
private static PinArgs GeneratePinDTO(Record record) => new(record);
private static void CallJSMethod(IMapHandler handler, string script)
{
if (handler.PlatformView is WebView2 webView2 && webView2.CoreWebView2 != null)
{
_ = webView2.DispatcherQueue.TryEnqueue(async () => await webView2.ExecuteScriptAsync(script));
}
}
private static string GetMapHtmlPage(string key, bool disableStreetView, bool usingCustomPins, double symbolSize) =>
$$$"""
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline' https://*.bing.com https://*.virtualearth.net; style-src 'self' 'unsafe-inline' https://*.bing.com https://*.virtualearth.net; media-src *">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<style>
body, html{
padding:0;
margin:0;
}
</style>
</head>
<body onload='loadMap();'>
<div id="myMap"></div>
<script src="https://www.bing.com/api/maps/mapcontrol?key={{{key}}}"></script>
<script defer>
var map;
var anchor;
var mainLayer;
var dragLayer;
var isCloning;
var selectedPin;
var locationPin;
var originalRecords;
const symbolSize = {{{symbolSize}}};
const usingCustomPins = {{{usingCustomPins.ToStringLower()}}};
const svgCache = new Map();
function loadMap() {
anchor = new Microsoft.Maps.Point(symbolSize / 2, symbolSize / 2);
map = new Microsoft.Maps.Map(document.getElementById('myMap'), {
disableBirdseye : true,
disableZooming: true,
// disablePanning: true,
disableStreetside: {{{disableStreetView.ToStringLower()}}},
showScalebar: false,
showLocateMeButton: false,
showDashboard: false,
showTermsLink: false,
showTrafficButton: false
});
Microsoft.Maps.Events.addHandler(map, 'viewrendered', invokeBoundsArgs);
Microsoft.Maps.Events.addHandler(map, "dblclick", handleDblClick);
}
function handleDblClick(e) {
var clickArgs = {
messageType: '{{{nameof(ClickArgs)}}}',
location: e.location,
identifier: 'unknown',
invokeClickEvent: true
};
invokeHandlerAction(clickArgs);
}
function toggleMapZoom(enable) {
map.setOptions({
disableZooming: !enable,
});
}
function togglePanning(enable) {
map.setOptions({
disablePanning: !enable,
});
}
function toggleTraffic(enable) {
map.setOptions({
disableTraffic: !enable,
});
}
function setMapType(mauiMapType) {
let mapTypeID;
switch(mauiMapType) {
case 'Street':
mapTypeID = Microsoft.Maps.MapTypeId.road;
break;
case 'Satellite':
mapTypeID = Microsoft.Maps.MapTypeId.aerial;
break;
case 'Hybrid':
mapTypeID = Microsoft.Maps.MapTypeId.aerial;
break;
default:
mapTypeID = Microsoft.Maps.MapTypeId.road;
}
map.setView({
mapTypeId: mapTypeID
});
}
function setRegion(southWestLat, southWestLong, northEastLat, northEastLong) {
const southWest = new Microsoft.Maps.Location(southWestLat, southWestLong);
const northEast = new Microsoft.Maps.Location(northEastLat, northEastLong);
const locations = [southWest, northEast];
const rect = Microsoft.Maps.LocationRect.fromLocations(locations);
map.setView({ bounds: rect, padding: 80 });
}
function beginDragPin(id, isCloningTemp) {
isCloning = isCloningTemp;
if (!originalRecords || !Array.isArray(originalRecords) || originalRecords.length < 1) {
alert("Records not found");
endDragMode();
return;
}
const record = originalRecords.find(r => r.identifier == id);
if (!record) {
alert("cannot find original record for selected pin " + id);
endDragMode();
return;
}
const pinLocation = new Microsoft.Maps.Location(record.location.latitude, record.location.longitude);
const originalPinToDrag = mainLayer.getPrimitives().find(a => a.identifier == id);
if (!originalPinToDrag) {
alert("Couldn't find pin to drag");
endDragMode();
return;
}
let draggablePin;
if (usingCustomPins) {
const customIcon = createCustomSvgIcon(record.geometry, "yellow", record.fillRule);
draggablePin = new Microsoft.Maps.Pushpin(originalPinToDrag.getLocation(), { icon: customIcon, draggable: true, anchor: anchor });
}
else {
draggablePin = new Microsoft.Maps.Pushpin(originalPinToDrag.getLocation(), { color: 'yellow', draggable: true, anchor: anchor });
}
Microsoft.Maps.Events.addHandler(draggablePin, 'dragend', function(e) { endDragPin(e, id); });
if (!dragLayer) {
dragLayer = new Microsoft.Maps.Layer();
dragLayer.add(draggablePin);
map.layers.insert(dragLayer);
}
else {
dragLayer.clear();
dragLayer.add(draggablePin);
}
mainLayer.setVisible(false);
dragLayer.setVisible(true);
}
function endDragPin(e, id) {
const pin = e.target;
if (pin) {
let message = isCloning ? "Clone new pin at this location" : "Save new location for pin?";
if (!confirm(message)) {
endDragMode();
return;
}
if (isCloning)
{
invokeCloneArgs(e.location, id);
isCloning = false;
}
else
{
selectedPin.setLocation(e.location);
invokeClickArgs(e.location, id);
}
}
endDragMode();
}
function endDragMode() {
dragLayer.setVisible(false);
mainLayer.setVisible(true);
}
function changeRecordType(recordToUpdate) {
const originalRecord = originalRecords.find(r => r.identifier == recordToUpdate.identifier);
let pinToUpdate;
const isSelected = selectedPin.identifier == recordToUpdate.identifier;
console.log('isSelected: ' + isSelected);
if (isSelected) {
pinToUpdate = selectedPin;
}
else {
pinToUpdate = mainLayer.getPrimitives().find(a => a.identifier == recordToUpdate.identifier);
}
originalRecord.geometry = recordToUpdate.geometry;
originalRecord.fillRule = recordToUpdate.fillRule;
const customIcon = createCustomSvgIcon(originalRecord.geometry, "yellow", originalRecord.fillRule);
pinToUpdate.setOptions({ icon: customIcon, anchor: anchor });
}
function addPins(records) {
originalRecords = records;
const isNew = !mainLayer;
if (isNew) {
mainLayer = new Microsoft.Maps.Layer();
}
else {
mainLayer.clear();
}
records.forEach(record => {
const newPin = generateNewPin(record);
if (!newPin) {
return;
}
newPin.originalColor = record.pinColor;
newPin.identifier = record.identifier;
mainLayer.add(newPin);
});
if (isNew) {
map.layers.insert(mainLayer);
}
Microsoft.Maps.Events.addHandler(mainLayer, 'click', handleClick);
Microsoft.Maps.Events.addHandler(mainLayer, 'rightclick', handleClick);
}
function generateNewPin(record) {
const location = new Microsoft.Maps.Location(record.location.latitude, record.location.longitude);
if (usingCustomPins) {
const customIcon = createCustomSvgIcon(record.geometry, record.pinColor, record.fillRule);
return new Microsoft.Maps.Pushpin(location, { icon: customIcon, anchor: anchor });
}
else {
return new Microsoft.Maps.Pushpin(location, { color: record.pinColor });
}
}
function handleClick(e) {
const newSelectedPin = e.primitive;
if (!(newSelectedPin instanceof Microsoft.Maps.Pushpin)) {
return; // shape is not a pushPin
}
if (selectedPin === newSelectedPin) {
console.log("pin was already selected");
return;
}
invokeClickArgs(newSelectedPin.getLocation(), newSelectedPin.identifier);
if (usingCustomPins) {
updateCustomPinColor(selectedPin, 0);
updateCustomPinColor(newSelectedPin, Number.MAX_SAFE_INTEGER, "yellow");
} else {
if (selectedPin) {
selectedPin.setOptions({ color: selectedPin.originalColor, zIndex: 0 });
}
newSelectedPin.setOptions({ color: 'yellow', zIndex: Number.MAX_SAFE_INTEGER });
}
selectedPin = newSelectedPin;
}
function updateCustomPinColor(pin, zIndex, newPinColor = null) {
if (pin) {
const record = originalRecords.find(r => r.identifier == pin.identifier);
if (record) {
const pinColor = newPinColor == null ? record.pinColor : newPinColor;
const customIcon = createCustomSvgIcon(record.geometry, pinColor, record.fillRule);
pin.setOptions({ icon: customIcon, anchor: anchor, zIndex: zIndex });
}
}
}
function unsetSelectedPin() {
if (selectedPin) {
selectedPin.setOptions({ color: selectedPin.originalColor, zIndex: 0 });
selectedPin = null;
}
}
function invokeBoundsArgs() {
const bounds = map.getBounds();
bounds.messageType = '{{{nameof(Bounds)}}}';
invokeHandlerAction(bounds);
}
function invokeClickArgs(location, identifier) {
const clickArgs = {
messageType: '{{{nameof(ClickArgs)}}}',
identifier: identifier,
location: location,
};
invokeHandlerAction(clickArgs);
}
function invokeCloneArgs(location, identifier) {
const cloneArgs = {
messageType: '{{{nameof(CloneArgs)}}}',
recordIdToClone: identifier,
cloneLocation: location
};
invokeHandlerAction(cloneArgs);
}
function setLocationPin(latitude, longitude) {
if(!locationPin)
{
let location = new Microsoft.Maps.Location(latitude, longitude);
locationPin = new Microsoft.Maps.Pushpin(location, { title: 'Current Location' });
}
map.entities.push(locationPin);
}
function removeLocationPin() {
if(locationPin != null)
{
map.entities.remove(locationPin);
locationPin = null;
}
}
function createCustomSvgIcon(pathData, fillColor, fillRule) {
// Validate pathData
if (typeof pathData !== 'string' || pathData.length === 0 || !/^m|M/.test(pathData)) {
throw new Error('Invalid pathData. Ensure the record contains all the geometry information from the symbol');
}
// Generate a unique cache key based on the input parameters
const cacheKey = `${pathData}-${fillColor}-${fillRule}-${symbolSize}`;
// Check if the result is already in the cache and return it if available
if (svgCache.has(cacheKey)) {
return svgCache.get(cacheKey);
}
// Create an offscreen SVG and path elements
const tempSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pathData);
tempSvg.appendChild(path);
// Set the offscreen SVG element's style to avoid affecting the layout
tempSvg.style.position = "absolute";
tempSvg.style.visibility = "hidden";
tempSvg.style.width = "0";
tempSvg.style.height = "0";
// Add the offscreen SVG element to the DOM to calculate the path's bounding box
document.body.appendChild(tempSvg);
// Get the bounding box
const { x, y, width, height } = path.getBBox();
// Remove the offscreen SVG element from the DOM
document.body.removeChild(tempSvg);
// Calculate the scaling and translation required to fit the path within the viewBox
const scale = 1000 / Math.max(width, height);
const translateX = (1000 - width * scale) / 2 - x * scale;
const translateY = (1000 - height * scale) / 2 - y * scale;
// Create the final SVG with the appropriate scaling and translation applied to the path
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid meet" width="${symbolSize}" height="${symbolSize}"><path d="${pathData}" fill-rule="${fillRule}" fill="${fillColor}" transform="translate(${translateX},${translateY}) scale(${scale})"/></svg>`;
// Store the generated SVG string in the cache and return it
svgCache.set(cacheKey, svg);
return svg;
}
function invokeHandlerAction(data) {
window.chrome.webview.postMessage(data);
}
</script>
</body>
</html>
""";
private static async ValueTask<Location?> GetCurrentLocation()
{
var locationWhenInUse = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
if (locationWhenInUse != PermissionStatus.Granted)
{
return Constants.DefaultLocation;
}
var geolocator = new Geolocator() { DesiredAccuracy = PositionAccuracy.High };
var position = await geolocator.GetGeopositionAsync();
return new Location(position.Coordinate.Latitude, position.Coordinate.Longitude);
}
private static void ProcessNonClickEvent(CustomMap customMap, ClickArgs clickArgs)
{
if (customMap.MapState is MapState.Moving or MapState.Cloning)
{
customMap.MovePin(clickArgs.Identifier, clickArgs.Location);
return;
}
customMap.SelectedViaJavascript = true;
UpdateSelectedItem(customMap, clickArgs.Identifier);
}
private static void UpdateSelectedItem(CustomMap customMap, string identifier)
{
var recordId = Ulid.Parse(identifier);
var newSelectedItem = customMap.ItemsSource.Cast<Record>().FirstOrDefault(i => i.Id == recordId);
customMap.SelectedItem = newSelectedItem != customMap.SelectedItem ? newSelectedItem : null;
}
private static bool ProcessWebEvent<T>(CoreWebView2WebMessageReceivedEventArgs args, Action<T> action)
{
try
{
var result = JsonSerializer.Deserialize<T>(args.WebMessageAsJson, JsonOptions);
if (result is not null)
{
action.Invoke(result);
return true;
}
}
catch (JsonException)
{
// If not the right event, then skip
}
return false;
}
private void WebViewNavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
if (this.isWebViewLoaded || !args.IsSuccess)
{
return;
}
this.isWebViewLoaded = true;
// Update initial properties when our page is loaded
CustomMapper.UpdateProperties(this, this.VirtualView);
if (this.regionToGo is not null)
{
MapMoveToRegion(this, this.VirtualView, this.regionToGo);
}
}
private void WebViewWebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
if (args.WebMessageAsJson is "undefined" || string.IsNullOrWhiteSpace(args.WebMessageAsJson))
{
return;
}
if (ProcessWebEvent<Bounds>(args, this.UpdateVisibleRegion))
{
return;
}
if (ProcessWebEvent<ClickArgs>(args, this.ProcessClickArgs))
{
return;
}
if (ProcessWebEvent<CloneArgs>(args, this.ProcessCloneArgs))
{
return;
}
throw new InvalidDataException($"Cannot parse the following message from WebView: {args.WebMessageAsJson}");
}
private void UpdateVisibleRegion(Bounds mapRect)
{
if (mapRect.Center is null)
{
return;
}
var location = new Location(mapRect.Center.Latitude, mapRect.Center.Longitude);
this.VirtualView.VisibleRegion = new MapSpan(location, mapRect.Height, mapRect.Width);
}
private void ProcessClickArgs(ClickArgs clickArgs)
{
var customMap = (CustomMap)this.VirtualView;
if (clickArgs.InvokeClickEvent)
{
this.VirtualView.Clicked(clickArgs.Location);
}
else
{
ProcessNonClickEvent(customMap, clickArgs);
}
}
private void ProcessCloneArgs(CloneArgs cloneArgs)
{
var customMap = (CustomMap)this.VirtualView;
if (customMap.MapState is MapState.Cloning)
{
customMap.ClonePin(cloneArgs.RecordIdToClone, cloneArgs.CloneLocation);
}
}
private sealed class CloneArgs
{
public string MessageType => nameof(CloneArgs);
public required string RecordIdToClone { get; set; }
public required Location CloneLocation { get; set; }
}
private sealed class ClickArgs
{
public string MessageType => nameof(ClickArgs);
public required string Identifier { get; set; }
public required Location Location { get; set; }
public bool InvokeClickEvent { get; set; }
}
private sealed class Bounds
{
public string MessageType => nameof(Bounds);
public required Location Center { get; set; }
public required double Width { get; set; }
public required double Height { get; set; }
}
} |
Description of Change
This is the initial work to support the Map feature on Windows, since there's no official MapControl yet on WinUI we provide this webview based implementation for now.
Fixes:
#605
PR Checklist
approved
(bug) orChampioned
(feature/proposal)main
at time of PRAdditional information