diff --git a/BlazorLeaflet/BlazorLeaflet.Samples/Pages/Index.razor b/BlazorLeaflet/BlazorLeaflet.Samples/Pages/Index.razor index a1183e9..c8d693b 100644 --- a/BlazorLeaflet/BlazorLeaflet.Samples/Pages/Index.razor +++ b/BlazorLeaflet/BlazorLeaflet.Samples/Pages/Index.razor @@ -14,11 +14,14 @@ +
+

Map bounds: @_map.Bounds?.ToString()

+ @code { @@ -67,6 +70,7 @@ }; _map.AddLayer(_circle); }; + _map.OnBoundsChanged += (s, e) => this.StateHasChanged(); } private LatLng _startAt = new LatLng(47.5574007f, 16.3918687f); @@ -102,12 +106,18 @@ private void ZoomMap() { - _map.FitBounds(new PointF(45.943f, 24.967f), new PointF(46.943f, 25.967f), maxZoom: 5f); - } - - private void PanToNY() - { - _map.PanTo(new PointF(40.713185f, -74.0072333f), animate: true, duration: 10f); + _map.FitBounds(new PointF(45.943f, 24.967f), new PointF(46.943f, 25.967f), maxZoom: 5f); + } + + private void PanToNY() + { + _map.PanTo(new PointF(40.713185f, -74.0072333f), animate: true, duration: 10f); + } + + private void SetToNY() + { + _map.Center = new LatLng(new PointF(40.713185f, -74.0072333f)); + _map.Zoom = 15; } } diff --git a/BlazorLeaflet/BlazorLeaflet/LeafletInterops.cs b/BlazorLeaflet/BlazorLeaflet/LeafletInterops.cs index 369338b..94a726e 100644 --- a/BlazorLeaflet/BlazorLeaflet/LeafletInterops.cs +++ b/BlazorLeaflet/BlazorLeaflet/LeafletInterops.cs @@ -1,6 +1,7 @@ -using BlazorLeaflet.Models; +using BlazorLeaflet.Models; using Microsoft.JSInterop; using System; +using System.Collections.Generic; using System.Drawing; using System.Threading.Tasks; using Rectangle = BlazorLeaflet.Models.Rectangle; @@ -10,29 +11,56 @@ namespace BlazorLeaflet public static class LeafletInterops { + private static Dictionary LayerReferences { get; } + = new Dictionary(); + private static readonly string _BaseObjectContainer = "window.leafletBlazor"; public static ValueTask Create(IJSRuntime jsRuntime, Map map) => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.create", map, DotNetObjectReference.Create(map)); - public static ValueTask AddLayer(IJSRuntime jsRuntime, string mapId, Layer layer) => - layer switch + private static DotNetObjectReference CreateLayerReference(string mapId, T layer) where T : Layer + { + var result = DotNetObjectReference.Create(layer); + LayerReferences.Add(layer.Id, (result, mapId, layer)); + return result; + } + + private static void DisposeLayerReference(string layerId) + { + if (LayerReferences.TryGetValue(layerId, out var value)) + { + value.Item1.Dispose(); + LayerReferences.Remove(layerId); + } + } + + public static ValueTask AddLayer(IJSRuntime jsRuntime, string mapId, Layer layer) + { + return layer switch { - TileLayer tileLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addTilelayer", mapId, tileLayer, DotNetObjectReference.Create(tileLayer)), - MbTilesLayer mbTilesLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addMbTilesLayer", mapId, mbTilesLayer, DotNetObjectReference.Create(mbTilesLayer)), - ShapefileLayer shapefileLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addShapefileLayer", mapId, shapefileLayer, DotNetObjectReference.Create(shapefileLayer)), - Marker marker => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addMarker", mapId, marker, DotNetObjectReference.Create(marker)), - Rectangle rectangle => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addRectangle", mapId, rectangle, DotNetObjectReference.Create(rectangle)), - Circle circle => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addCircle", mapId, circle, DotNetObjectReference.Create(circle)), - Polygon polygon => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addPolygon", mapId, polygon, DotNetObjectReference.Create(polygon)), - Polyline polyline => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addPolyline", mapId, polyline, DotNetObjectReference.Create(polyline)), - ImageLayer image => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addImageLayer", mapId, image, DotNetObjectReference.Create(image)), - GeoJsonDataLayer geo => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addGeoJsonLayer", mapId, geo, DotNetObjectReference.Create(geo)), + TileLayer tileLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addTilelayer", mapId, tileLayer, CreateLayerReference(mapId, tileLayer)), + MbTilesLayer mbTilesLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addMbTilesLayer", mapId, mbTilesLayer, CreateLayerReference(mapId, mbTilesLayer)), + ShapefileLayer shapefileLayer => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addShapefileLayer", mapId, shapefileLayer, CreateLayerReference(mapId, shapefileLayer)), + Marker marker => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addMarker", mapId, marker, CreateLayerReference(mapId, marker)), + Rectangle rectangle => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addRectangle", mapId, rectangle, CreateLayerReference(mapId, rectangle)), + Circle circle => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addCircle", mapId, circle, CreateLayerReference(mapId, circle)), + Polygon polygon => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addPolygon", mapId, polygon, CreateLayerReference(mapId, polygon)), + Polyline polyline => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addPolyline", mapId, polyline, CreateLayerReference(mapId, polyline)), + ImageLayer image => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addImageLayer", mapId, image, CreateLayerReference(mapId, image)), + GeoJsonDataLayer geo => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.addGeoJsonLayer", mapId, geo, CreateLayerReference(mapId, geo)), _ => throw new NotImplementedException($"The layer {typeof(Layer).Name} has not been implemented."), - }; + }; + } - public static ValueTask RemoveLayer(IJSRuntime jsRuntime, string mapId, string layerId) => - jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.removeLayer", mapId, layerId); + public static async ValueTask RemoveLayer(IJSRuntime jsRuntime, string mapId, string layerId) + { + await jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.removeLayer", mapId, layerId); + DisposeLayerReference(layerId); + } + + public static ValueTask SetMarkerIcon(IJSRuntime jsRuntime, string mapId, string layerId, Icon icon) + => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.setMarkerIcon", mapId, layerId, icon); public static ValueTask UpdatePopupContent(IJSRuntime jsRuntime, string mapId, Layer layer) => jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.updatePopupContent", mapId, layer.Id, layer.Popup?.Content); @@ -61,5 +89,23 @@ layer switch public static ValueTask GetZoom(IJSRuntime jsRuntime, string mapId) => jsRuntime.InvokeAsync($"{_BaseObjectContainer}.getZoom", mapId); + + private class _Bounds + { + public LatLng _southWest { get; set; } + public LatLng _northEast { get; set; } + + public Bounds AsBounds() => new Bounds(_southWest, _northEast); + } + + public static async Task GetBounds(IJSRuntime jsRuntime, string mapId) + { + return (await jsRuntime.InvokeAsync<_Bounds>($"{_BaseObjectContainer}.getBounds", mapId)).AsBounds(); + } + + public static async Task SetZoom(IJSRuntime jsRuntime, string mapId, float zoomLevel) + { + await jsRuntime.InvokeVoidAsync($"{_BaseObjectContainer}.setZoom", mapId, zoomLevel); + } } } diff --git a/BlazorLeaflet/BlazorLeaflet/Map.cs b/BlazorLeaflet/BlazorLeaflet/Map.cs index 84cd33c..58e793b 100644 --- a/BlazorLeaflet/BlazorLeaflet/Map.cs +++ b/BlazorLeaflet/BlazorLeaflet/Map.cs @@ -15,16 +15,44 @@ namespace BlazorLeaflet { public class Map { + private LatLng _Center = new LatLng(); /// - /// Initial geographic center of the map + /// Geographic center of the map /// - public LatLng Center { get; set; } = new LatLng(); + /// + public LatLng Center + { + get => _Center; + set + { + _Center = value; + if (_isInitialized) + RunTaskInBackground(async () => await LeafletInterops.PanTo( + _jsRuntime, Id, value.ToPointF(), false, 0, 0, false)); + } + } + private float _Zoom; /// - /// Initial map zoom level + /// Map zoom level /// - public float Zoom { get; set; } - + public float Zoom + { + get => _Zoom; + set + { + _Zoom = value; + if (_isInitialized) + RunTaskInBackground(async () => await LeafletInterops.SetZoom( + _jsRuntime, Id, value)); + } + } + + /// + /// Map bounds + /// + public Bounds Bounds { get; private set; } + /// /// Minimum zoom level of the map. If not specified and at least one /// GridLayer or TileLayer is in the map, the lowest of their minZoom @@ -74,6 +102,19 @@ public void RaiseOnInitialized() { _isInitialized = true; OnInitialized?.Invoke(); + RunTaskInBackground(UpdateBounds); + } + + private async void RunTaskInBackground(Func task) + { + try + { + await task(); + } + catch (Exception ex) + { + NotifyBackgroundExceptionOccurred(ex); + } } /// @@ -171,6 +212,26 @@ public void PanTo(PointF position, bool animate = false, float duration = 0.25f, public async Task GetCenter() => await LeafletInterops.GetCenter(_jsRuntime, Id); public async Task GetZoom() => await LeafletInterops.GetZoom(_jsRuntime, Id); + public async Task GetBounds() => await LeafletInterops.GetBounds(_jsRuntime, Id); + + private async Task UpdateZoom() + { + _Zoom = await GetZoom(); + await UpdateBounds(); + } + + private async Task UpdateCenter() + { + + _Center = await GetCenter(); + await UpdateBounds(); + } + + private async Task UpdateBounds() + { + Bounds = await GetBounds(); + OnBoundsChanged?.Invoke(this, new EventArgs()); + } #region events @@ -215,11 +276,33 @@ public void PanTo(PointF position, bool animate = false, float duration = 0.25f, public event MapEventHandler OnZoomEnd; [JSInvokable] - public void NotifyZoomEnd(Event e) => OnZoomEnd?.Invoke(this, e); + public async void NotifyZoomEnd(Event e) + { + try + { + await UpdateZoom(); + } + finally + { + OnZoomEnd?.Invoke(this, e); + } + } public event MapEventHandler OnMoveEnd; [JSInvokable] - public void NotifyMoveEnd(Event e) => OnMoveEnd?.Invoke(this, e); + public async void NotifyMoveEnd(Event e) + { + try + { + await UpdateCenter(); + } + finally + { + OnMoveEnd?.Invoke(this, e); + } + } + + public event EventHandler OnBoundsChanged; public event MouseEventHandler OnMouseMove; [JSInvokable] @@ -241,6 +324,10 @@ public void PanTo(PointF position, bool animate = false, float duration = 0.25f, [JSInvokable] public void NotifyPreClick(MouseEvent eventArgs) => OnPreClick?.Invoke(this, eventArgs); + public event EventHandler BackgroundExceptionOccurred; + private void NotifyBackgroundExceptionOccurred(Exception exception) => + BackgroundExceptionOccurred?.Invoke(this, exception); + #endregion events #region InteractiveLayerEvents diff --git a/BlazorLeaflet/BlazorLeaflet/Models/Bounds.cs b/BlazorLeaflet/BlazorLeaflet/Models/Bounds.cs new file mode 100644 index 0000000..d05c0e7 --- /dev/null +++ b/BlazorLeaflet/BlazorLeaflet/Models/Bounds.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace BlazorLeaflet.Models +{ + public class Bounds + { + [DataMember(Name = "_northEast")] + public LatLng NorthEast { get; set; } + + [DataMember(Name = "_southWest")] + public LatLng SouthWest { get; set; } + + public Bounds() { } + public Bounds(LatLng southWest, LatLng northEast) + { + NorthEast = northEast; + SouthWest = southWest; + } + + public override string ToString() => + $"NE: {NorthEast.Lat} N, {NorthEast.Lng} E; SW: {SouthWest.Lat} N, {SouthWest.Lng} E"; + } +} diff --git a/BlazorLeaflet/BlazorLeaflet/wwwroot/leafletBlazorInterops.js b/BlazorLeaflet/BlazorLeaflet/wwwroot/leafletBlazorInterops.js index 88c53e1..0c04a65 100644 --- a/BlazorLeaflet/BlazorLeaflet/wwwroot/leafletBlazorInterops.js +++ b/BlazorLeaflet/BlazorLeaflet/wwwroot/leafletBlazorInterops.js @@ -1,4 +1,4 @@ -maps = {}; +maps = {}; layers = {}; window.leafletBlazor = { @@ -206,6 +206,12 @@ window.leafletBlazor = { }, getZoom: function (mapId) { return maps[mapId].getZoom(); + }, + getBounds: function (mapId) { + return maps[mapId].getBounds(); + }, + setZoom: function (mapId, zoomLevel) { + maps[mapId].setZoom(zoomLevel); } };