From 59e3b487dc00ba7545215b080dcc50408a45e81e Mon Sep 17 00:00:00 2001 From: Nicholas Brostrom Date: Mon, 28 Oct 2024 17:01:58 +0100 Subject: [PATCH 1/5] New component for rendering advanced markers --- .../Pages/AdvancedMarkerComponent.razor | 113 ++++++++++ ClientSideDemo/Shared/CoolMarker.razor | 30 +++ ClientSideDemo/Shared/NavMenu.razor | 6 +- ClientSideDemo/wwwroot/css/site.css | 56 +++++ GoogleMapsComponents/AdvancedGoogleMap.razor | 80 +++++++ GoogleMapsComponents/GoogleMap.razor | 37 +--- GoogleMapsComponents/MapComponent.cs | 2 + .../Maps/AdvancedMarkerView.cs | 2 +- GoogleMapsComponents/Maps/IMarker.cs | 6 + GoogleMapsComponents/Maps/Marker.cs | 2 +- GoogleMapsComponents/Maps/MarkerClustering.cs | 32 +-- .../Maps/MarkerComponent.razor | 3 + .../Maps/MarkerComponent.razor.cs | 205 ++++++++++++++++++ .../Maps/MarkerComponentRef.cs | 8 + .../wwwroot/js/objectManager.js | 90 ++++++-- 15 files changed, 593 insertions(+), 79 deletions(-) create mode 100644 ClientSideDemo/Pages/AdvancedMarkerComponent.razor create mode 100644 ClientSideDemo/Shared/CoolMarker.razor create mode 100644 GoogleMapsComponents/AdvancedGoogleMap.razor create mode 100644 GoogleMapsComponents/Maps/IMarker.cs create mode 100644 GoogleMapsComponents/Maps/MarkerComponent.razor create mode 100644 GoogleMapsComponents/Maps/MarkerComponent.razor.cs create mode 100644 GoogleMapsComponents/Maps/MarkerComponentRef.cs diff --git a/ClientSideDemo/Pages/AdvancedMarkerComponent.razor b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor new file mode 100644 index 00000000..a0bd427f --- /dev/null +++ b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor @@ -0,0 +1,113 @@ +@page "/AdvancedMarkerComponent" +@using GoogleMapsComponents +@using GoogleMapsComponents.Maps +@using GoogleMapsComponents.Maps.Coordinates +@using GoogleMapsComponents.Maps.Extension + + @foreach (var markerRef in Markers.Where(x => x.Visible)) + { + + + + } + + +@foreach (var marker in Markers) +{ +
+

@marker.Id -> (@marker.Lat x @marker.Lng)

+ + + +
+} + + + + +@code { + private List Markers = + [ + new MarkerData { Id = 1, Lat = 13.505892, Lng = 100.8162 }, + new MarkerData { Id = 2, Lng = 150.363181, Lat = -33.718234 }, + new MarkerData { Id = 3, Lng = 150.371124, Lat = -33.727111 }, + new MarkerData { Id = 4, Lng = 151.209834, Lat = -33.848588 }, + new MarkerData { Id = 5, Lng = 151.216968, Lat = -33.851702 }, + new MarkerData { Id = 6, Lng = 150.863657, Lat = -34.671264 }, + new MarkerData { Id = 7, Lng = 148.662905, Lat = -35.304724 }, + new MarkerData { Id = 8, Lng = 175.699196, Lat = -36.817685 }, + new MarkerData { Id = 9, Lng = 175.790222, Lat = -36.828611 }, + new MarkerData { Id = 10, Lng = 145.116667, Lat = -37.75 } + ]; + + private AdvancedGoogleMap _map1 = null!; + MarkerClustering? _markerClustering; + private readonly MapOptions _mapOptions = new MapOptions() + { + Zoom = 13, + Center = new LatLngLiteral() + { + Lat = 13.505892, + Lng = 100.8162 + }, + IsFractionalZoomEnabled = false, + HeadingInteractionEnabled = true, + CameraControl = true, + MapTypeId = MapTypeId.Roadmap, + // ColorScheme = ColorScheme.Dark, + MapId = "e5asd595q2121" + }; + + async Task InvokeClustering() + { + if (_map1.MapRef is null) return; + if (_markerClustering == null) + { + _markerClustering = await MarkerClustering.CreateAsync(_map1.MapRef.JsRuntime, _map1.InteropObject!, _map1.Markers, new MarkerClustererOptions() + { + ZoomOnClick = true + // RendererObjectName = "customRendererLib.interpolatedRenderer" + }); + } + else + { + await _markerClustering.ClearMarkers(); + await _markerClustering.AddMarkers(_map1.Markers); + } + } + + async Task ClearClustering() + { + if (_markerClustering != null) + { + await _markerClustering.ClearMarkers(); + await _markerClustering.DisposeAsync(); + _markerClustering = null; + } + } + + public class MarkerData + { + public int Id { get; set; } + public double Lat { get; set; } + public double Lng { get; set; } + public bool Clickable { get; set; } = true; + public bool Draggable { get; set; } + + public bool Visible { get; set; } = true; + public bool Active { get; set; } + + public void UpdatePosition(LatLngLiteral position) + { + Lat = position.Lat; + Lng = position.Lng; + } + } +} \ No newline at end of file diff --git a/ClientSideDemo/Shared/CoolMarker.razor b/ClientSideDemo/Shared/CoolMarker.razor new file mode 100644 index 00000000..4f69145e --- /dev/null +++ b/ClientSideDemo/Shared/CoolMarker.razor @@ -0,0 +1,30 @@ +@using ClientSideDemo.Pages +@if (Marker.Active) +{ +
+

I am now active

+

You could render literally anything
with just default blazor

+

This is just a simple example that
shows of marker with label

+

Current at: @Marker.Lat x @Marker.Lng

+
+} +else +{ +
+
+
+
+ @($"Name {Marker.Id}") + + error + +
+
+
+ +
+} + +@code { + [Parameter] public AdvancedMarkerComponent.MarkerData Marker { get; set; } = default!; +} \ No newline at end of file diff --git a/ClientSideDemo/Shared/NavMenu.razor b/ClientSideDemo/Shared/NavMenu.razor index e7d10a11..93daf0b8 100644 --- a/ClientSideDemo/Shared/NavMenu.razor +++ b/ClientSideDemo/Shared/NavMenu.razor @@ -47,7 +47,11 @@ MapRoutes - + diff --git a/ClientSideDemo/wwwroot/css/site.css b/ClientSideDemo/wwwroot/css/site.css index 8d6a61b8..634db247 100644 --- a/ClientSideDemo/wwwroot/css/site.css +++ b/ClientSideDemo/wwwroot/css/site.css @@ -133,3 +133,59 @@ app { display: block; } } + +.label { + position: relative; + display: inline-block; + background-color: #fff; + padding: 8px 14px; + line-height: 22px; + min-height: 35px; +} + +.label::before { + content: ""; + position: absolute; + top: -4px; + left: 0; + right: 0; + margin: 0 auto; + height: 4px; + width: calc(100% - 20px); + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + + +.label-content { + display: flex; + justify-items: center; + justify-content: space-between; + min-width: 120px; + font-family: sans-serif; + font-size: 16px; +} + +.label-arrowLeft::after { + content: ""; + position: absolute; + bottom: 0; + left: -8px; + width: 0; + height: 0; + border-style: solid; + border-width: 8px 8px 0 0; + border-color: transparent #fff transparent transparent; + -webkit-transform: rotate(360deg); +} + +.label-arrowLeft { + border-radius: 10px 10px 10px 0; + box-shadow: -2px 6px 20px rgba(0, 0, 0, .45); +} + +.label-icon { + display: flex; + height: 20px; + margin-left: 8px; +} \ No newline at end of file diff --git a/GoogleMapsComponents/AdvancedGoogleMap.razor b/GoogleMapsComponents/AdvancedGoogleMap.razor new file mode 100644 index 00000000..4c2bdf97 --- /dev/null +++ b/GoogleMapsComponents/AdvancedGoogleMap.razor @@ -0,0 +1,80 @@ +@using GoogleMapsComponents.Maps +@using Microsoft.JSInterop +@implements IAsyncDisposable + + + +@if (MapRef?.InteropObject is not null) +{ + + @ChildContent + +} + +@code { + // Due to us wrapping the normal map, keep this public to still be able to access the interop. + public GoogleMap? MapRef; + + // Expose this for simplicity. + public Map? InteropObject => MapRef?.InteropObject; + + internal Guid? MapId => MapRef?.InteropObject.Guid; + + public IEnumerable Markers => MapComponents.Select(x => x.Value.ToMarker()); + internal readonly Dictionary MapComponents = []; + internal DotNetObjectReference? callbackRef; + + [Parameter] + public string? Id { get; set; } + + [Parameter] + public MapOptions? Options { get; set; } + + [Parameter] + public EventCallback OnAfterInit { get; set; } + + [Parameter] + public string? CssClass { get; set; } + + [Parameter] + public string? Height { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + protected override void OnInitialized() + { + callbackRef = DotNetObjectReference.Create(this); + base.OnInitialized(); + } + + private async Task AfterInit() + { + + await OnAfterInit.InvokeAsync(); + } + + [JSInvokable] + public async Task OnMarkerClicked(Guid markerId) + { + if (MapComponents.TryGetValue(markerId, out var markerComponent)) + await markerComponent.MarkerClicked(); + } + + [JSInvokable] + public async Task OnMarkerDrag(Guid markerId, LatLngLiteral position) + { + if (MapComponents.TryGetValue(markerId, out var markerComponent)) + await markerComponent.MarkerDragged(position); + } + + public async ValueTask DisposeAsync() + { + // Mark components as disposed, since they will be removed by disposing the MapRef. + foreach (var component in MapComponents) + component.Value.IsDisposed = true; + if (MapRef != null) await MapRef.DisposeAsync(); + callbackRef?.Dispose(); + } + +} \ No newline at end of file diff --git a/GoogleMapsComponents/GoogleMap.razor b/GoogleMapsComponents/GoogleMap.razor index c93af004..4ad10795 100644 --- a/GoogleMapsComponents/GoogleMap.razor +++ b/GoogleMapsComponents/GoogleMap.razor @@ -5,17 +5,11 @@ @using Microsoft.JSInterop @inherits MapComponent -@implements IDisposable @inject IJSRuntime JSRuntime
@code { - #nullable enable - // Load the module and keep a reference to it - // You need to use .AsTask() to convert the ValueTask to Task as it may be awaited multiple times - //private List moduleImports = new List(); - [Parameter] public string? Id { get; set; } @@ -52,33 +46,12 @@ { if (firstRender) { - //var tasks = new List>(); - //tasks.Add(JSRuntime.InvokeAsync("import", "./_content/BlazorGoogleMaps/js/objectManager.js").AsTask()); - //if(!string.IsNullOrWhiteSpace(ApiKey)) - // tasks.Add(JSRuntime.InvokeAsync("import", $"https://maps.googleapis.com/maps/api/js?key={ApiKey}&v=3").AsTask()); - - //moduleImports.AddRange(await Task.WhenAll(tasks.ToArray())); + await InitAsync(Element, Options); + await OnAfterInit.InvokeAsync(); } - - await InitAsync(Element, Options); - - //Debug.WriteLine("Init finished"); - - await OnAfterInit.InvokeAsync(); + + await base.OnAfterRenderAsync(firstRender); } - protected override bool ShouldRender() - { - return false; - } - - void IDisposable.Dispose() - { - //if(moduleImports != null && moduleImports.Count > 0) - //{ - // foreach (var mi in moduleImports) - // await mi.DisposeAsync(); - //} - base.Dispose(); - } + protected override bool ShouldRender() => false; } \ No newline at end of file diff --git a/GoogleMapsComponents/MapComponent.cs b/GoogleMapsComponents/MapComponent.cs index 6a013059..d8c29b8a 100644 --- a/GoogleMapsComponents/MapComponent.cs +++ b/GoogleMapsComponents/MapComponent.cs @@ -17,6 +17,7 @@ public class MapComponent : ComponentBase, IDisposable, IAsyncDisposable public IServiceProvider ServiceProvider { get; protected set; } = default!; private IBlazorGoogleMapsKeyService? _keyService; + internal event EventHandler? MapInitialized; protected override void OnInitialized() { @@ -38,6 +39,7 @@ public async Task InitAsync(ElementReference element, MapOptions? options = null } InteropObject = await Map.CreateAsync(JsRuntime, element, options); + MapInitialized?.Invoke(this, EventArgs.Empty); } public async ValueTask DisposeAsync() diff --git a/GoogleMapsComponents/Maps/AdvancedMarkerView.cs b/GoogleMapsComponents/Maps/AdvancedMarkerView.cs index 52cc3daf..f201a2e0 100644 --- a/GoogleMapsComponents/Maps/AdvancedMarkerView.cs +++ b/GoogleMapsComponents/Maps/AdvancedMarkerView.cs @@ -8,7 +8,7 @@ namespace GoogleMapsComponents.Maps; /// 2023-09 /// Notice: Available only in the v=beta channel. /// -public class AdvancedMarkerElement : ListableEntityBase +public class AdvancedMarkerElement : ListableEntityBase, IMarker { // https://developers.google.com/maps/documentation/javascript/reference/3.55/advanced-markers public const string GoogleMapAdvancedMarkerName = "google.maps.marker.AdvancedMarkerElement"; diff --git a/GoogleMapsComponents/Maps/IMarker.cs b/GoogleMapsComponents/Maps/IMarker.cs new file mode 100644 index 00000000..722702ef --- /dev/null +++ b/GoogleMapsComponents/Maps/IMarker.cs @@ -0,0 +1,6 @@ +namespace GoogleMapsComponents.Maps; + +public interface IMarker : IJsObjectRef +{ + //Empty interface, but allows us to make specific classes "markers" +} \ No newline at end of file diff --git a/GoogleMapsComponents/Maps/Marker.cs b/GoogleMapsComponents/Maps/Marker.cs index 6c664e65..30ce8893 100644 --- a/GoogleMapsComponents/Maps/Marker.cs +++ b/GoogleMapsComponents/Maps/Marker.cs @@ -4,7 +4,7 @@ namespace GoogleMapsComponents.Maps; -public class Marker : ListableEntityBase +public class Marker : ListableEntityBase, IMarker { public static async Task CreateAsync(IJSRuntime jsRuntime, MarkerOptions? opts = null) { diff --git a/GoogleMapsComponents/Maps/MarkerClustering.cs b/GoogleMapsComponents/Maps/MarkerClustering.cs index af89ed1c..2019dd61 100644 --- a/GoogleMapsComponents/Maps/MarkerClustering.cs +++ b/GoogleMapsComponents/Maps/MarkerClustering.cs @@ -18,7 +18,7 @@ public class MarkerClustering : EventEntityBase, IJsObjectRef public static async Task CreateAsync( IJSRuntime jsRuntime, Map map, - IEnumerable markers, + IEnumerable markers, MarkerClustererOptions? options = null ) { @@ -31,22 +31,6 @@ public static async Task CreateAsync( return obj; } - public static async Task CreateAsync( - IJSRuntime jsRuntime, - Map map, - IEnumerable advancedMarkerElements, - MarkerClustererOptions? options = null - ) - { - options ??= new MarkerClustererOptions(); - - var guid = Guid.NewGuid(); - var jsObjectRef = new JsObjectRef(jsRuntime, guid); - await jsRuntime.InvokeVoidAsync("blazorGoogleMaps.objectManager.createClusteringMarkers", guid.ToString(), map.Guid.ToString(), advancedMarkerElements, options); - var obj = new MarkerClustering(jsObjectRef); - return obj; - } - internal MarkerClustering(JsObjectRef jsObjectRef) : base(jsObjectRef) { } @@ -56,7 +40,7 @@ internal MarkerClustering(JsObjectRef jsObjectRef) : base(jsObjectRef) /// /// /// when true, clusters will not be rerendered on the next map idle event rather than immediately after markers are added - public virtual async Task AddMarkers(IEnumerable? markers, bool noDraw = false) + public virtual async Task AddMarkers(IEnumerable? markers, bool noDraw = false) { if (markers == null) { @@ -66,16 +50,6 @@ public virtual async Task AddMarkers(IEnumerable? markers, bool noDraw = await _jsObjectRef.JSRuntime.InvokeVoidAsync("blazorGoogleMaps.objectManager.addClusteringMarkers", _jsObjectRef.Guid.ToString(), markers, noDraw); } - public virtual async Task AddMarkers(IEnumerable? advancedMarkerElements, bool noDraw = false) - { - if (advancedMarkerElements == null) - { - return; - } - - await _jsObjectRef.JSRuntime.InvokeVoidAsync("blazorGoogleMaps.objectManager.addClusteringMarkers", _jsObjectRef.Guid.ToString(), advancedMarkerElements, noDraw); - } - public virtual async Task SetMap(Map map) { await _jsObjectRef.InvokeAsync("setMap", map); @@ -110,6 +84,4 @@ public virtual Task Render() { return _jsObjectRef.InvokeAsync("render"); } - - } \ No newline at end of file diff --git a/GoogleMapsComponents/Maps/MarkerComponent.razor b/GoogleMapsComponents/Maps/MarkerComponent.razor new file mode 100644 index 00000000..ceed0fe4 --- /dev/null +++ b/GoogleMapsComponents/Maps/MarkerComponent.razor @@ -0,0 +1,3 @@ +
+ @ChildContent +
diff --git a/GoogleMapsComponents/Maps/MarkerComponent.razor.cs b/GoogleMapsComponents/Maps/MarkerComponent.razor.cs new file mode 100644 index 00000000..bdca78f2 --- /dev/null +++ b/GoogleMapsComponents/Maps/MarkerComponent.razor.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace GoogleMapsComponents.Maps; + +public partial class MarkerComponent : IAsyncDisposable +{ + public MarkerComponent() + { + Guid = Guid.NewGuid(); + _componentId = "marker_" + Guid.ToString("N"); + } + private readonly string _componentId; + private bool hasRendered = false; + internal bool IsDisposed = false; + + public Guid Guid { get; } + + [Inject] + private IJSRuntime JS { get; set; } = default!; + + [CascadingParameter(Name = "Map")] + private AdvancedGoogleMap MapRef { get; set; } = default!; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Latitude in degrees. Values will be clamped to the range [-90, 90]. + /// This means that if the value specified is less than -90, it will be set to -90. + /// And if the value is greater than 90, it will be set to 90. + /// + [Parameter] + public double Lat { get; set; } + + /// + /// Longitude in degrees. Values outside the range [-180, 180] will be wrapped so that they fall within the range. + /// For example, a value of -190 will be converted to 170. A value of 190 will be converted to -170. + /// This reflects the fact that longitudes wrap around the globe. + /// + [Parameter] + public double Lng { get; set; } + + /// + /// An enumeration specifying how an AdvancedMarkerElement should behave when it collides with another AdvancedMarkerElement or with the basemap labels on a vector map. + /// Note: AdvancedMarkerElement to AdvancedMarkerElement collision works on both raster and vector maps, however, AdvancedMarkerElement to base map's label collision only works on vector maps. + /// + [Parameter] + public CollisionBehavior? CollisionBehavior { get; set; } + + /// + /// If true, the AdvancedMarkerElement can be dragged. + /// Note: AdvancedMarkerElement with altitude is not draggable. + /// + [Parameter] + public bool Draggable { get; set; } + + /// + /// This event is fired when the user stops moving the marker. + /// + [Parameter] + public EventCallback OnMove { get; set; } + + /// + /// If true, the AdvancedMarkerElement will be clickable and trigger the gmp-click event, and will be interactive for accessibility purposes (e.g. allowing keyboard navigation via arrow keys). + /// + [Parameter] + public bool Clickable { get; set; } + + /// + /// This event is fired when the marker is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// Rollover text. If provided, an accessibility text (e.g. for use with screen readers) will be added to the + /// + [Parameter] + public string? Title { get; set; } + + /// + /// All entities are displayed on the map in order of their zIndex, with higher values displaying in front of entities with lower values. + /// By default, entities are displayed according to their vertical position on screen, with lower entities appearing in front of entities further up the screen. + /// + [Parameter] + public int? ZIndex { get; set; } + + public IMarker ToMarker() + { + return new MarkerComponentRef() + { + Guid = Guid + }; + } + + internal async Task MarkerClicked() + { + await OnClick.InvokeAsync(); + } + + internal async Task MarkerDragged(LatLngLiteral position) + { + await OnMove.InvokeAsync(position); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + MapRef.MapComponents[Guid] = this; + hasRendered = true; + await UpdateOptions(); + } + await base.OnAfterRenderAsync(firstRender); + } + + private async Task UpdateOptions() + { + await JS.InvokeAsync("blazorGoogleMaps.objectManager.updateAdvancedComponent", Guid, new AdvancedMarkerComponentOptions() + { + CollisionBehavior = CollisionBehavior, + Position = new LatLngLiteral(Lat, Lng), + ComponentId = _componentId, + Title = Title ?? "", + GmpClickable = Clickable, + GmpDraggable = Draggable, + MapId = MapRef.MapId, + ZIndex = ZIndex + }, MapRef.callbackRef); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + if (!hasRendered) + { + await base.SetParametersAsync(parameters); + return; + } + + var optionsChanged = parameters.DidParameterChange(CollisionBehavior) || + parameters.DidParameterChange(Lat) || + parameters.DidParameterChange(Lng) || + parameters.DidParameterChange(ZIndex) || + parameters.DidParameterChange(Title) || + parameters.DidParameterChange(Clickable) || + parameters.DidParameterChange(Draggable); + + await base.SetParametersAsync(parameters); + + if (optionsChanged) + { + await UpdateOptions(); + } + } + + public async ValueTask DisposeAsync() + { + if (IsDisposed) return; + IsDisposed = true; + await JS.InvokeVoidAsync("blazorGoogleMaps.objectManager.disposeAdvancedMarkerComponent", Guid); + MapRef.MapComponents.Remove(Guid); + GC.SuppressFinalize(this); + } + + internal readonly struct AdvancedMarkerComponentOptions + { + public LatLngLiteral? Position { get; init; } + public Guid? MapId { get; init; } + public CollisionBehavior? CollisionBehavior { get; init; } + public required string ComponentId { get; init; } + public bool GmpDraggable { get; init; } + public bool GmpClickable { get; init; } + public string? Title { get; init; } + public int? ZIndex { get; init; } + } +} + +/// +/// Contains extension methods for . +/// +internal static class ParameterViewExtensions +{ + /// + /// Checks if a parameter changed. + /// + /// The value type + /// The parameters. + /// Name of the parameter. + /// The parameter value (SHOULD NOT BE ENTERED MANUALLY). + /// true if the parameter value has changed, false otherwise. + internal static bool DidParameterChange(this ParameterView parameters, T parameterValue, [CallerArgumentExpression("parameterValue")] string parameterName = "") + { + if (parameters.TryGetValue(parameterName, out T? value) && value != null) + { + return !EqualityComparer.Default.Equals(value, parameterValue); + } + + return false; + } +} diff --git a/GoogleMapsComponents/Maps/MarkerComponentRef.cs b/GoogleMapsComponents/Maps/MarkerComponentRef.cs new file mode 100644 index 00000000..b63cd780 --- /dev/null +++ b/GoogleMapsComponents/Maps/MarkerComponentRef.cs @@ -0,0 +1,8 @@ +using System; + +namespace GoogleMapsComponents.Maps; + +public class MarkerComponentRef : IMarker +{ + public Guid Guid { get; init; } +} \ No newline at end of file diff --git a/GoogleMapsComponents/wwwroot/js/objectManager.js b/GoogleMapsComponents/wwwroot/js/objectManager.js index 5a16bfdf..4da17150 100644 --- a/GoogleMapsComponents/wwwroot/js/objectManager.js +++ b/GoogleMapsComponents/wwwroot/js/objectManager.js @@ -347,7 +347,7 @@ let args2 = args.slice(2).map(arg => tryParseJson(arg)); let functionName = args[1]; - let advancedMarkerElementContent = getAdvancedMarkerElementContent(functionName, args2.length > 0 ? args2[0].content : null); + let advancedMarkerElementContent = getAdvancedMarkerElementContent(functionName, args2.length > 0 ? args2[0]?.content : null); if (advancedMarkerElementContent !== null) { args2[0].content = advancedMarkerElementContent; } @@ -372,8 +372,8 @@ let guids = JSON.parse(args[0]); - for (var i = 0, len = args2.length; i < len; i++) { - var constructorArgs = args2[i]; + for (let i = 0, len = args2.length; i < len; i++) { + const constructorArgs = args2[i]; let advancedMarkerElementContent = getAdvancedMarkerElementContent(functionName, constructorArgs.content); if (advancedMarkerElementContent !== null) { constructorArgs.content = advancedMarkerElementContent; @@ -487,23 +487,25 @@ var arr = map.overlayMapTypes.clear(); }, disposeMapElements(mapGuid) { - var keysToRemove = []; + const keysToRemove = []; - for (var key in mapObjects) { + for (const key in mapObjects) { if (mapObjects.hasOwnProperty(key)) { - var element = mapObjects[key]; - if (element.hasOwnProperty("map") - && element.hasOwnProperty("guidString") - && element.map !== null - && element.map !== undefined - && element.map.guidString === mapGuid) { + const element = mapObjects[key]; + if ( + "guidString" in element && // Element has a guidString property (inherited is important for advanced marker) + "map" in element && // Element has a map property (inherited is important for advanced marker) + element.map && // Element has a map + "guidString" in element.map && // Map has a guidString + element.map.guidString === mapGuid // The guidString is matching our current guidString + ) { keysToRemove.push(element.guidString); } } } - for (var keyToRemove in keysToRemove) { + for (const keyToRemove in keysToRemove) { if (keysToRemove.hasOwnProperty(keyToRemove)) { - var elementToRemove = keysToRemove[keyToRemove]; + const elementToRemove = keysToRemove[keyToRemove]; delete mapObjects[elementToRemove]; } } @@ -516,7 +518,7 @@ delete controlParents[mapGuid]; } - if (controlParents !== null && Object.keys(controlParents) == 0) { + if (controlParents !== null && Object.keys(controlParents) === 0) { controlParents = null; } }, @@ -878,6 +880,66 @@ }); mapObjects[guid].addMarkers(originalMarkers, noDraw); + }, + updateAdvancedComponent: function (id, options, callbackRef) { + const collisionBehaviorMapping = [ + google.maps.CollisionBehavior.REQUIRED, + google.maps.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL, + google.maps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY + ]; + const existingMarker = mapObjects[id]; + if (existingMarker) { + existingMarker.position = options.position; + existingMarker.title = options.title; + existingMarker.zIndex = options.zIndex; + existingMarker.collisionBehavior = collisionBehaviorMapping[options.collisionBehavior]; + const clickChanged = existingMarker.gmpClickable !== options.gmpClickable; + const dragChanged = existingMarker.gmpDraggable !== options.gmpDraggable; + existingMarker.gmpClickable = options.gmpClickable; + existingMarker.gmpDraggable = options.gmpDraggable; + if (clickChanged) { + if (options.gmpClickable) { + existingMarker.addEventListener("gmp-click", _ => { + callbackRef?.invokeMethodAsync('OnMarkerClicked', id); + }) + } + else{ + existingMarker.removeEventListener("gmp-click") + } + } + return; + } + const map = mapObjects[options.mapId]; + const content = document.querySelector(`#${options.componentId}`); + if (!content) return null; // Should never be reached? + const advancedMarkerElement = new google.maps.marker.AdvancedMarkerElement({ + map, + content, + position: options.position, + title: options.title, + zIndex: options.zIndex, + gmpClickable: options.gmpClickable, + gmpDraggable: options.gmpDraggable, + collisionBehavior: collisionBehaviorMapping[options.collisionBehavior] + }); + advancedMarkerElement.guidString = id; + if (options.gmpClickable) { + advancedMarkerElement.addEventListener("gmp-click", _ => { + callbackRef?.invokeMethodAsync('OnMarkerClicked', id); + }) + } + //Always add this event, since it's not removable + advancedMarkerElement.addListener('dragend', (event) => { + if (!advancedMarkerElement.gmpDraggable) return; + callbackRef?.invokeMethodAsync('OnMarkerDrag', id, advancedMarkerElement.position); + }); + addMapObject(id, advancedMarkerElement); + }, + disposeAdvancedMarkerComponent: function (id) { + const existingMarker = mapObjects[id]; + if (!existingMarker) return; + existingMarker.map = null; + this.disposeObject(id); } } }; From 9c4b264bc8db1e1e5281000e2cf3a5055ac00df2 Mon Sep 17 00:00:00 2001 From: Nicholas Brostrom Date: Wed, 30 Oct 2024 16:01:29 +0100 Subject: [PATCH 2/5] Add some documentation --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 8791bf10..0ac13d3f 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,64 @@ If you want to use marker clustering add this script as well: } ``` +OR Render markers with Blazor (currently only with `v=beta` version of google-maps, and specify a `MapId`) +``` +@page "/map" +@using GoogleMapsComponents +@using GoogleMapsComponents.Maps + +

Google Map

+ + @foreach (var markerRef in Markers) + { + +

I am a blazor component

+
+ } +
+@code { + private List Markers = + [ + new MarkerData { Id = 1, Lat = 13.505892, Lng = 100.8162 }, + ]; + private AdvancedGoogleMap? _map1; + private MapOptions mapOptions =new MapOptions() + { + Zoom = 13, + Center = new LatLngLiteral() + { + Lat = 13.505892, + Lng = 100.8162 + }, + MapId = "DEMO_MAP_ID", //required for blazor markers + MapTypeId = MapTypeId.Roadmap + }; + + public class MarkerData + { + public int Id { get; set; } + public double Lat { get; set; } + public double Lng { get; set; } + public bool Clickable { get; set; } = true; + public bool Draggable { get; set; } + public bool Active { get; set; } + + public void UpdatePosition(LatLngLiteral position) + { + Lat = position.Lat; + Lng = position.Lng; + } + } +} +``` + ## Samples Please check server side samples https://github.com/rungwiroon/BlazorGoogleMaps/tree/master/ServerSideDemo which are most to date From ec85f54f936ff8d200b5276b9db0175e5e5f09a6 Mon Sep 17 00:00:00 2001 From: Nicholas Brostrom Date: Thu, 31 Oct 2024 09:26:57 +0100 Subject: [PATCH 3/5] Restructure and refactor --- .../Pages/AdvancedMarkerComponent.razor | 3 +- .../Maps/Extension/ParameterViewExtensions.cs | 29 ++++++++++++++ .../Maps/MarkerComponent.razor | 2 +- .../Maps/MarkerComponent.razor.cs | 35 +++++----------- .../wwwroot/js/objectManager.js | 40 +++++++++++-------- 5 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 GoogleMapsComponents/Maps/Extension/ParameterViewExtensions.cs diff --git a/ClientSideDemo/Pages/AdvancedMarkerComponent.razor b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor index a0bd427f..384da015 100644 --- a/ClientSideDemo/Pages/AdvancedMarkerComponent.razor +++ b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor @@ -13,7 +13,8 @@ Clickable="@markerRef.Clickable" Draggable="@markerRef.Draggable" OnClick="@(() => markerRef.Active = !markerRef.Active)" - OnMove="pos => markerRef.UpdatePosition(pos)"> + OnMove="pos => markerRef.UpdatePosition(pos)" + data-iscool="@true"> } diff --git a/GoogleMapsComponents/Maps/Extension/ParameterViewExtensions.cs b/GoogleMapsComponents/Maps/Extension/ParameterViewExtensions.cs new file mode 100644 index 00000000..6f69b7a2 --- /dev/null +++ b/GoogleMapsComponents/Maps/Extension/ParameterViewExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Components; + +namespace GoogleMapsComponents.Maps.Extension; + +/// +/// Contains extension methods for . +/// +internal static class ParameterViewExtensions +{ + /// + /// Checks if a parameter changed. + /// + /// The value type + /// The parameters. + /// Name of the parameter. + /// The parameter value (SHOULD NOT BE ENTERED MANUALLY). + /// true if the parameter value has changed, false otherwise. + internal static bool DidParameterChange(this ParameterView parameters, T parameterValue, [CallerArgumentExpression("parameterValue")] string parameterName = "") + { + if (parameters.TryGetValue(parameterName, out T? value) && value != null) + { + return !EqualityComparer.Default.Equals(value, parameterValue); + } + + return false; + } +} \ No newline at end of file diff --git a/GoogleMapsComponents/Maps/MarkerComponent.razor b/GoogleMapsComponents/Maps/MarkerComponent.razor index ceed0fe4..6f38edf7 100644 --- a/GoogleMapsComponents/Maps/MarkerComponent.razor +++ b/GoogleMapsComponents/Maps/MarkerComponent.razor @@ -1,3 +1,3 @@ -
+
@ChildContent
diff --git a/GoogleMapsComponents/Maps/MarkerComponent.razor.cs b/GoogleMapsComponents/Maps/MarkerComponent.razor.cs index bdca78f2..6b6fa519 100644 --- a/GoogleMapsComponents/Maps/MarkerComponent.razor.cs +++ b/GoogleMapsComponents/Maps/MarkerComponent.razor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading.Tasks; +using GoogleMapsComponents.Maps.Extension; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -90,6 +90,13 @@ public MarkerComponent() [Parameter] public int? ZIndex { get; set; } + /// + /// Specifies additional custom attributes that will be rendered on the "root" component of the marker. + /// + /// The attributes. + [Parameter(CaptureUnmatchedValues = true)] + public IReadOnlyDictionary Attributes { get; set; } = default!; + public IMarker ToMarker() { return new MarkerComponentRef() @@ -178,28 +185,4 @@ internal readonly struct AdvancedMarkerComponentOptions public string? Title { get; init; } public int? ZIndex { get; init; } } -} - -/// -/// Contains extension methods for . -/// -internal static class ParameterViewExtensions -{ - /// - /// Checks if a parameter changed. - /// - /// The value type - /// The parameters. - /// Name of the parameter. - /// The parameter value (SHOULD NOT BE ENTERED MANUALLY). - /// true if the parameter value has changed, false otherwise. - internal static bool DidParameterChange(this ParameterView parameters, T parameterValue, [CallerArgumentExpression("parameterValue")] string parameterName = "") - { - if (parameters.TryGetValue(parameterName, out T? value) && value != null) - { - return !EqualityComparer.Default.Equals(value, parameterValue); - } - - return false; - } -} +} \ No newline at end of file diff --git a/GoogleMapsComponents/wwwroot/js/objectManager.js b/GoogleMapsComponents/wwwroot/js/objectManager.js index 4da17150..2ff5c6ed 100644 --- a/GoogleMapsComponents/wwwroot/js/objectManager.js +++ b/GoogleMapsComponents/wwwroot/js/objectManager.js @@ -887,31 +887,36 @@ google.maps.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL, google.maps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY ]; + const updateMarkerProperties = (marker, options) => { + marker.position = options.position; + marker.title = options.title; + marker.zIndex = options.zIndex; + marker.collisionBehavior = collisionBehaviorMapping[options.collisionBehavior]; + marker.gmpClickable = options.gmpClickable; + marker.gmpDraggable = options.gmpDraggable; + }; + const handleMarkerClick = (marker, options) => { + if (options.gmpClickable) { + marker.addEventListener("gmp-click", () => { + callbackRef?.invokeMethodAsync('OnMarkerClicked', id); + }); + } else { + marker.removeEventListener("gmp-click"); + } + }; const existingMarker = mapObjects[id]; if (existingMarker) { - existingMarker.position = options.position; - existingMarker.title = options.title; - existingMarker.zIndex = options.zIndex; - existingMarker.collisionBehavior = collisionBehaviorMapping[options.collisionBehavior]; const clickChanged = existingMarker.gmpClickable !== options.gmpClickable; const dragChanged = existingMarker.gmpDraggable !== options.gmpDraggable; - existingMarker.gmpClickable = options.gmpClickable; - existingMarker.gmpDraggable = options.gmpDraggable; - if (clickChanged) { - if (options.gmpClickable) { - existingMarker.addEventListener("gmp-click", _ => { - callbackRef?.invokeMethodAsync('OnMarkerClicked', id); - }) - } - else{ - existingMarker.removeEventListener("gmp-click") - } - } + updateMarkerProperties(existingMarker, options); + + if (clickChanged) handleMarkerClick(existingMarker, options); return; } const map = mapObjects[options.mapId]; const content = document.querySelector(`#${options.componentId}`); - if (!content) return null; // Should never be reached? + if (!content) console.warn("Marker tried to render without a target component"); // Should never be reached? + const advancedMarkerElement = new google.maps.marker.AdvancedMarkerElement({ map, content, @@ -933,6 +938,7 @@ if (!advancedMarkerElement.gmpDraggable) return; callbackRef?.invokeMethodAsync('OnMarkerDrag', id, advancedMarkerElement.position); }); + addMapObject(id, advancedMarkerElement); }, disposeAdvancedMarkerComponent: function (id) { From acbd37ed752c29f6744a62d866b4bee97f261eae Mon Sep 17 00:00:00 2001 From: Nicholas Brostrom Date: Thu, 31 Oct 2024 09:32:08 +0100 Subject: [PATCH 4/5] Add sample to server side to make sure it works as expected there aswell --- .../Pages/AdvancedMarkerComponent.razor | 112 ++++++++++++++++++ ServerSideDemo/Shared/CoolMarker.razor | 30 +++++ ServerSideDemo/Shared/NavMenu.razor | 5 + ServerSideDemo/wwwroot/css/site.css | 57 +++++++++ 4 files changed, 204 insertions(+) create mode 100644 ServerSideDemo/Pages/AdvancedMarkerComponent.razor create mode 100644 ServerSideDemo/Shared/CoolMarker.razor diff --git a/ServerSideDemo/Pages/AdvancedMarkerComponent.razor b/ServerSideDemo/Pages/AdvancedMarkerComponent.razor new file mode 100644 index 00000000..9649ee97 --- /dev/null +++ b/ServerSideDemo/Pages/AdvancedMarkerComponent.razor @@ -0,0 +1,112 @@ +@page "/AdvancedMarkerComponent" +@using GoogleMapsComponents +@using GoogleMapsComponents.Maps + + @foreach (var markerRef in Markers.Where(x => x.Visible)) + { + + + + } + + +@foreach (var marker in Markers) +{ +
+

@marker.Id -> (@marker.Lat x @marker.Lng)

+ + + +
+} + + + + +@code { + private List Markers = + [ + new MarkerData { Id = 1, Lat = 13.505892, Lng = 100.8162 }, + new MarkerData { Id = 2, Lng = 150.363181, Lat = -33.718234 }, + new MarkerData { Id = 3, Lng = 150.371124, Lat = -33.727111 }, + new MarkerData { Id = 4, Lng = 151.209834, Lat = -33.848588 }, + new MarkerData { Id = 5, Lng = 151.216968, Lat = -33.851702 }, + new MarkerData { Id = 6, Lng = 150.863657, Lat = -34.671264 }, + new MarkerData { Id = 7, Lng = 148.662905, Lat = -35.304724 }, + new MarkerData { Id = 8, Lng = 175.699196, Lat = -36.817685 }, + new MarkerData { Id = 9, Lng = 175.790222, Lat = -36.828611 }, + new MarkerData { Id = 10, Lng = 145.116667, Lat = -37.75 } + ]; + + private AdvancedGoogleMap _map1 = null!; + MarkerClustering? _markerClustering; + private readonly MapOptions _mapOptions = new MapOptions() + { + Zoom = 13, + Center = new LatLngLiteral() + { + Lat = 13.505892, + Lng = 100.8162 + }, + IsFractionalZoomEnabled = false, + HeadingInteractionEnabled = true, + CameraControl = true, + MapTypeId = MapTypeId.Roadmap, + // ColorScheme = ColorScheme.Dark, + MapId = "e5asd595q2121" + }; + + async Task InvokeClustering() + { + if (_map1.MapRef is null) return; + if (_markerClustering == null) + { + _markerClustering = await MarkerClustering.CreateAsync(_map1.MapRef.JsRuntime, _map1.InteropObject!, _map1.Markers, new MarkerClustererOptions() + { + ZoomOnClick = true + // RendererObjectName = "customRendererLib.interpolatedRenderer" + }); + } + else + { + await _markerClustering.ClearMarkers(); + await _markerClustering.AddMarkers(_map1.Markers); + } + } + + async Task ClearClustering() + { + if (_markerClustering != null) + { + await _markerClustering.ClearMarkers(); + await _markerClustering.DisposeAsync(); + _markerClustering = null; + } + } + + public class MarkerData + { + public int Id { get; set; } + public double Lat { get; set; } + public double Lng { get; set; } + public bool Clickable { get; set; } = true; + public bool Draggable { get; set; } + + public bool Visible { get; set; } = true; + public bool Active { get; set; } + + public void UpdatePosition(LatLngLiteral position) + { + Lat = position.Lat; + Lng = position.Lng; + } + } +} \ No newline at end of file diff --git a/ServerSideDemo/Shared/CoolMarker.razor b/ServerSideDemo/Shared/CoolMarker.razor new file mode 100644 index 00000000..9d4abd00 --- /dev/null +++ b/ServerSideDemo/Shared/CoolMarker.razor @@ -0,0 +1,30 @@ +@using ServerSideDemo.Pages +@if (Marker.Active) +{ +
+

I am now active

+

You could render literally anything
with just default blazor

+

This is just a simple example that
shows of marker with label

+

Current at: @Marker.Lat x @Marker.Lng

+
+} +else +{ +
+
+
+
+ @($"Name {Marker.Id}") + + error + +
+
+
+ +
+} + +@code { + [Parameter] public AdvancedMarkerComponent.MarkerData Marker { get; set; } = default!; +} \ No newline at end of file diff --git a/ServerSideDemo/Shared/NavMenu.razor b/ServerSideDemo/Shared/NavMenu.razor index e60ba75b..f07e3967 100644 --- a/ServerSideDemo/Shared/NavMenu.razor +++ b/ServerSideDemo/Shared/NavMenu.razor @@ -128,6 +128,11 @@ Dispose Circle List +