diff --git a/ClientSideDemo/Pages/AdvancedMarkerComponent.razor b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor
new file mode 100644
index 00000000..c4b5f89d
--- /dev/null
+++ b/ClientSideDemo/Pages/AdvancedMarkerComponent.razor
@@ -0,0 +1,123 @@
+@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 OnMarkersChanged()
+ {
+ if (_markerClustering != null)
+ {
+ 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}")
+
+
+
+
+
+
+
+
+}
+
+@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
-
+
+
+ AdvancedMarkerComponent
+
+
diff --git a/ClientSideDemo/Startup.cs b/ClientSideDemo/Startup.cs
index b98e84b9..ef8dc210 100644
--- a/ClientSideDemo/Startup.cs
+++ b/ClientSideDemo/Startup.cs
@@ -13,7 +13,7 @@ private static async Task Main(string[] args)
builder.Services.AddBlazorGoogleMaps(new MapApiLoadOptions("AIzaSyBdkgvniMdyFPAcTlcZivr8f30iU-kn1T0")
{
- Version = "beta"
+ Version = "weekly"
});
builder.RootComponents.Add("app");
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..42a71783
--- /dev/null
+++ b/GoogleMapsComponents/AdvancedGoogleMap.razor
@@ -0,0 +1,95 @@
+@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 int MarkerCount => MapComponents.Count;
+ public IEnumerable Markers => MapComponents.Select(x => x.Value);
+ private 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 EventCallback OnMarkersChanged { 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);
+ }
+
+ internal void AddMarker(MarkerComponent marker)
+ {
+ MapComponents.TryAdd(marker.Guid, marker);
+ OnMarkersChanged.InvokeAsync();
+ }
+
+ internal void RemoveMarker(MarkerComponent marker)
+ {
+ MapComponents.Remove(marker.Guid);
+ OnMarkersChanged.InvokeAsync();
+ }
+
+ 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/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/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 83202b6d..7b5a3380 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..786b2e53 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);
@@ -84,12 +58,7 @@ public virtual async Task SetMap(Map map)
///
/// Removes provided markers from the clusterer's internal list of source markers.
///
- public virtual async Task RemoveMarkers(IEnumerable markers, bool noDraw = false)
- {
- await _jsObjectRef.JSRuntime.InvokeVoidAsync("blazorGoogleMaps.objectManager.removeClusteringMarkers", _jsObjectRef.Guid.ToString(), markers, noDraw);
- }
-
- public async Task RemoveMarkers(IEnumerable markers, bool noDraw = false)
+ public virtual async Task RemoveMarkers(IEnumerable markers, bool noDraw = false)
{
await _jsObjectRef.JSRuntime.InvokeVoidAsync("blazorGoogleMaps.objectManager.removeClusteringMarkers", _jsObjectRef.Guid.ToString(), markers, noDraw);
}
@@ -110,6 +79,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..6f38edf7
--- /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..eca3c0fc
--- /dev/null
+++ b/GoogleMapsComponents/Maps/MarkerComponent.razor.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using GoogleMapsComponents.Maps.Extension;
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+
+namespace GoogleMapsComponents.Maps;
+
+public partial class MarkerComponent : IAsyncDisposable, IMarker
+{
+ public MarkerComponent()
+ {
+ _guid = Guid.NewGuid();
+ _componentId = "marker_" + _guid.ToString("N");
+ }
+ private readonly string _componentId;
+ private bool hasRendered = false;
+ internal bool IsDisposed = false;
+ private Guid _guid;
+
+ public Guid Guid => Id ?? _guid;
+
+ [Inject]
+ private IJSRuntime JS { get; set; } = default!;
+
+ [CascadingParameter(Name = "Map")]
+ private AdvancedGoogleMap MapRef { get; set; } = default!;
+
+ [Parameter]
+ [JsonIgnore]
+ public RenderFragment? ChildContent { get; set; }
+
+ [Parameter, JsonIgnore]
+ public Guid? Id { 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, JsonIgnore]
+ 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, JsonIgnore]
+ 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, JsonIgnore]
+ public CollisionBehavior? CollisionBehavior { get; set; }
+
+ ///
+ /// If true, the AdvancedMarkerElement can be dragged.
+ /// Note: AdvancedMarkerElement with altitude is not draggable.
+ ///
+ [Parameter, JsonIgnore]
+ public bool Draggable { get; set; }
+
+ ///
+ /// This event is fired when the user stops moving the marker.
+ ///
+ [Parameter, JsonIgnore]
+ 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, JsonIgnore]
+ public bool Clickable { get; set; }
+
+ ///
+ /// This event is fired when the marker is clicked.
+ ///
+ [Parameter, JsonIgnore]
+ 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, JsonIgnore]
+ 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, JsonIgnore]
+ public int? ZIndex { get; set; }
+
+ ///
+ /// A possible override MapId, if this is unset, the markers will read their MapId from the AdvancedGoogleMap
+ ///
+ [Parameter, JsonIgnore]
+ public Guid? MapId { get; set; }
+
+ ///
+ /// Specifies additional custom attributes that will be rendered on the "root" component of the marker.
+ ///
+ /// The attributes.
+ [Parameter(CaptureUnmatchedValues = true), JsonIgnore]
+ public IReadOnlyDictionary Attributes { get; set; } = default!;
+
+ 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.AddMarker(this);
+ hasRendered = true;
+ await UpdateOptions();
+ }
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ ///
+ /// Trigger a "update" of the component, by default the component will update automatically when parameters changes.
+ ///
+ public async Task ForceRender()
+ {
+ if (!hasRendered) return;
+ await UpdateOptions();
+ }
+
+ 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 = 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(MapId) ||
+ 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.RemoveMarker(this);
+ 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; }
+ }
+}
\ No newline at end of file
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..315bfb0a 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,84 @@
});
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 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.clickListener = marker.addListener("click", () => {
+ callbackRef?.invokeMethodAsync('OnMarkerClicked', id);
+ });
+ } else if (marker.clickListener) {
+ google.maps.event.removeListener(marker.clickListener);
+ delete marker.clickListener;
+ }
+ };
+ const handleMarkerDrag = (marker, options) => {
+ if (options.gmpDraggable) {
+ marker.dragListener = marker.addListener('dragend', (event) => {
+ callbackRef?.invokeMethodAsync('OnMarkerDrag', id, marker.position);
+ });
+ } else if (marker.dragListener) {
+ google.maps.event.removeListener(marker.dragListener);
+ delete marker.dragListener;
+ }
+ };
+ const existingMarker = mapObjects[id];
+ if (existingMarker) {
+ const clickChanged = existingMarker.gmpClickable !== options.gmpClickable;
+ const dragChanged = existingMarker.gmpDraggable !== options.gmpDraggable;
+ updateMarkerProperties(existingMarker, options);
+
+ if (clickChanged) handleMarkerClick(existingMarker, options);
+ if (dragChanged) handleMarkerDrag(existingMarker, options)
+ return;
+ }
+ const map = mapObjects[options.mapId];
+ const content = document.querySelector(`#${options.componentId}`);
+ 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,
+ 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.clickListener = advancedMarkerElement.addListener("click", _ => {
+ callbackRef?.invokeMethodAsync('OnMarkerClicked', id);
+ })
+ }
+ if (advancedMarkerElement.gmpDraggable) {
+ advancedMarkerElement.dragListener = advancedMarkerElement.addListener('dragend', (event) => {
+ 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);
}
}
};
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
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}")
+
+
+
+
+
+
+
+
+}
+
+@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
+
+
+ Advanced marker components
+
+
Test things
diff --git a/ServerSideDemo/wwwroot/css/site.css b/ServerSideDemo/wwwroot/css/site.css
index dcfa1db5..c801799e 100644
--- a/ServerSideDemo/wwwroot/css/site.css
+++ b/ServerSideDemo/wwwroot/css/site.css
@@ -149,3 +149,60 @@ 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;
+}