diff --git a/src/OpenLayers.Blazor.Demo.Components/OpenLayers.Blazor.Demo.Components.csproj b/src/OpenLayers.Blazor.Demo.Components/OpenLayers.Blazor.Demo.Components.csproj index 11b5ef4..44ddfeb 100644 --- a/src/OpenLayers.Blazor.Demo.Components/OpenLayers.Blazor.Demo.Components.csproj +++ b/src/OpenLayers.Blazor.Demo.Components/OpenLayers.Blazor.Demo.Components.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -22,6 +22,7 @@ + diff --git a/src/OpenLayers.Blazor.Demo.Components/Pages/EventsDemo.razor b/src/OpenLayers.Blazor.Demo.Components/Pages/EventsDemo.razor new file mode 100644 index 0000000..903a194 --- /dev/null +++ b/src/OpenLayers.Blazor.Demo.Components/Pages/EventsDemo.razor @@ -0,0 +1,87 @@ +@page "/events" + +
+
+

Events

+ @if (_map != null) + { +
+Zoom: @_map.Zoom
+Center: @_map.Center
+Extent: @_map.VisibleExtent
+Pointer: @_pointer
+
+ } +
+
+ @foreach (var m in _msg) + { + + @m
+
+ } +
+
+ + + +@code { + private OpenStreetMap? _map; + private Coordinate? _pointer; + private readonly List _msg = []; + + protected override void OnInitialized() + { + AddMessage("Page.OnInitialized"); + base.OnInitialized(); + } + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + AddMessage($"Page.OnAfterRender firstRender: {firstRender}"); + base.OnAfterRender(firstRender); + } + + private void OnCenterChanged(Coordinate c) + { + AddMessage($"CenterChanged {c}"); + } + + private void AddMessage(string msg) + { + _msg.Insert(0, msg); + } + + private void OnRenderComplete() + { + AddMessage("RenderComplete"); + } + + private void OnClick(Coordinate c) + { + AddMessage($"OnClick {c}"); + } + + private void VisibleExtentChanged(Extent e) + { + AddMessage($"VisibleExtentChanged {e}"); + } + + private void OnPointerMove(Coordinate c) + { + _pointer = c; + } + + private void ZoomChanged(double z) + { + AddMessage($"ZoomChanged {z}"); + } + +} \ No newline at end of file diff --git a/src/OpenLayers.Blazor.Demo.Components/Pages/FlightTracker.razor b/src/OpenLayers.Blazor.Demo.Components/Pages/FlightTracker.razor new file mode 100644 index 0000000..affeb05 --- /dev/null +++ b/src/OpenLayers.Blazor.Demo.Components/Pages/FlightTracker.razor @@ -0,0 +1,155 @@ +@page "/flighttracker" +@using Microsoft.Extensions.Logging +@using System.Timers +@using System.Runtime.InteropServices +@using System.Text.Json.Serialization +@rendermode Components.RenderMode.DefaultRenderMode +@implements IDisposable + +
+
+

Flight Tracker

+
+
+ @* CORS might make some troubles. Works best in Blazor Server Mode Demo solution (just a demo, never just create server sided timers). *@ +

Using markers to show flights using API https://api.adsb.lol.

+
+
+ + + + + + + + + + +@code { + private OpenStreetMap? _map; + + [Inject] + HttpClient _httpClient { get; set; } + + private Timer? _timer; + + [Inject] + private ILogger _logger { get; set; } + + private AdsbResponse? _flightData; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _map.Center = new Coordinate(2.79437, 51.89508); + _map.Zoom = 9; + await LoadTrackingData(_map.Center); + + _timer = new Timer(new TimeSpan(0, 0, 3)); + _timer.Elapsed += async (sender, args) => await LoadTrackingData(_map.Center); + _timer.Start(); + } + await base.OnAfterRenderAsync(firstRender); + } + + private async Task LoadTrackingData(Coordinate center) + { + try + { + if (_map == null || _map.VisibleExtent == null) + return; + + var c1 = new Coordinate(_map.VisibleExtent.X1, _map.VisibleExtent.Y1); + var c2 = new Coordinate(_map.VisibleExtent.X2, _map.VisibleExtent.Y2); + var visibleDistance = c1.DistanceTo(c2); + var nmVisibleRadius = visibleDistance / 2 * 0.5399568; // to nm + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.adsb.lol/v2/point/{center.Y}/{center.X}/{(int)nmVisibleRadius}"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Browser"))) + { + // disable CORS + request.SetBrowserRequestMode(BrowserRequestMode.NoCors); + request.Headers.Add("Access-Control-Allow-Origin", "*"); + request.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept"); + } + + var response = await _httpClient.SendAsync(request); + + _flightData = await response.Content.ReadFromJsonAsync(); + + foreach (var ac in _flightData.Flights) + { + var marker = _map.MarkersList.FirstOrDefault(p => p.Id == ac.Id); + + if (marker == null) + { + marker = new Marker(new Coordinate(ac.Lon, ac.Lat), "_content/OpenLayers.Blazor.Demo.Components/airplane.png", 512, 512, 256, 256) + { Scale = 0.05, Popup = true, Title = ac.Name, Rotation = ac.TrueHeading * Math.PI / 180 }; + marker.Id = ac.Id; + _map.MarkersList.Add(marker); + } + { + marker.Coordinate = new Coordinate(ac.Lon, ac.Lat); + marker.Rotation = ac.TrueHeading * Math.PI / 180; + await marker.UpdateShape(); + } + } + } + catch (Exception exp) + { + _logger.LogError(exp, "Error reading flight tracking data"); + } + + await InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + _timer?.Dispose(); + } + + public class AdsbResponse + { + [JsonPropertyName("ac")] + public AdsbFlight[] Flights { get; set; } + } + + public class AdsbFlight + { + [JsonPropertyName("hex")] + public string Id { get; set; } + + [JsonPropertyName("flight")] + public string? Name { get; set; } + + public double Lon { get; set; } + public double Lat { get; set; } + + [JsonPropertyName("t")] + public string? Type { get; set; } + + [JsonPropertyName("r")] + public string? Registration { get; set; } + + [JsonPropertyName("true_heading")] + public double? TrueHeading { get; set; } + + [JsonPropertyName("alt_baro")] + public object? AltitudeBaro { get; set; } + + [JsonPropertyName("gs")] + public double? GroundSpeed { get; set; } + } + +} \ No newline at end of file diff --git a/src/OpenLayers.Blazor.Demo.Components/Pages/MarkersDemo.razor b/src/OpenLayers.Blazor.Demo.Components/Pages/MarkersDemo.razor index 3851dd4..c105982 100644 --- a/src/OpenLayers.Blazor.Demo.Components/Pages/MarkersDemo.razor +++ b/src/OpenLayers.Blazor.Demo.Components/Pages/MarkersDemo.razor @@ -12,6 +12,7 @@ + diff --git a/src/OpenLayers.Blazor.Demo.Components/wwwroot/airplane.png b/src/OpenLayers.Blazor.Demo.Components/wwwroot/airplane.png new file mode 100644 index 0000000..beed0c2 Binary files /dev/null and b/src/OpenLayers.Blazor.Demo.Components/wwwroot/airplane.png differ diff --git a/src/OpenLayers.Blazor.Demo.Server/Components/Layout/NavMenu.razor b/src/OpenLayers.Blazor.Demo.Server/Components/Layout/NavMenu.razor index 913455e..f78b509 100644 --- a/src/OpenLayers.Blazor.Demo.Server/Components/Layout/NavMenu.razor +++ b/src/OpenLayers.Blazor.Demo.Server/Components/Layout/NavMenu.razor @@ -62,6 +62,18 @@ Draw Shapes + + + + diff --git a/src/OpenLayers.Blazor.Demo/Shared/NavMenu.razor b/src/OpenLayers.Blazor.Demo/Shared/NavMenu.razor index a757855..4fce7b0 100644 --- a/src/OpenLayers.Blazor.Demo/Shared/NavMenu.razor +++ b/src/OpenLayers.Blazor.Demo/Shared/NavMenu.razor @@ -68,6 +68,18 @@ Germany GDZ Demo + + + + diff --git a/src/OpenLayers.Blazor.Demo/staticwebapp.config.json b/src/OpenLayers.Blazor.Demo/staticwebapp.config.json new file mode 100644 index 0000000..9b21040 --- /dev/null +++ b/src/OpenLayers.Blazor.Demo/staticwebapp.config.json @@ -0,0 +1,9 @@ +{ + "navigationFallback": { + "rewrite": "/index.html" + }, + "globalHeaders": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS" + } +} \ No newline at end of file diff --git a/src/OpenLayers.Blazor/Internal/Marker.cs b/src/OpenLayers.Blazor/Internal/Marker.cs index a20219f..8f150fc 100644 --- a/src/OpenLayers.Blazor/Internal/Marker.cs +++ b/src/OpenLayers.Blazor/Internal/Marker.cs @@ -18,6 +18,8 @@ public Marker(Coordinate point) : this() public float[]? Anchor { get; set; } + public double? Rotation { get; set; } + public override int GetHashCode() { return HashCode.Combine(base.GetHashCode(), Size, Anchor); diff --git a/src/OpenLayers.Blazor/Map.razor.cs b/src/OpenLayers.Blazor/Map.razor.cs index 2c71d97..8e958a7 100644 --- a/src/OpenLayers.Blazor/Map.razor.cs +++ b/src/OpenLayers.Blazor/Map.razor.cs @@ -46,7 +46,7 @@ public Map() /// Zoom level of the map /// [Parameter] - public double Zoom { get; set; } = 2; + public double Zoom { get; set; } = 5; /// /// Event on zoom changes @@ -239,7 +239,7 @@ public bool AutoPopup /// ValueTask public async ValueTask DisposeAsync() { - MarkersList.CollectionChanged -= ShapesOnCollectionChanged; + MarkersList.CollectionChanged -= MarkersOnCollectionChanged; ShapesList.CollectionChanged -= ShapesOnCollectionChanged; LayersList.CollectionChanged -= LayersOnCollectionChanged; @@ -310,7 +310,7 @@ await _module.InvokeVoidAsync("MapOLInit", _mapId, _popupId, Defaults, Center.Va LayersList.Select(p => p.InternalLayer).ToArray(), Instance); - MarkersList.CollectionChanged += ShapesOnCollectionChanged; + MarkersList.CollectionChanged += MarkersOnCollectionChanged; ShapesList.CollectionChanged += ShapesOnCollectionChanged; LayersList.CollectionChanged += LayersOnCollectionChanged; } @@ -633,13 +633,20 @@ private void LayersOnCollectionChanged(object? sender, NotifyCollectionChangedEv Task.Run(async () => { - if (e.OldItems != null) - foreach (var oldLayer in e.OldItems.OfType()) - await _module.InvokeVoidAsync("MapOLRemoveLayer", _mapId, oldLayer.InternalLayer); + if (e.Action == NotifyCollectionChangedAction.Reset) + { + await _module.InvokeVoidAsync("MapOLSetLayers", _mapId, null); + } + else + { + if (e.OldItems != null) + foreach (var oldLayer in e.OldItems.OfType()) + await _module.InvokeVoidAsync("MapOLRemoveLayer", _mapId, oldLayer.InternalLayer); - if (e.NewItems != null) - foreach (var newLayer in e.NewItems.OfType()) - await _module.InvokeVoidAsync("MapOLAddLayer", _mapId, newLayer.InternalLayer); + if (e.NewItems != null) + foreach (var newLayer in e.NewItems.OfType()) + await _module.InvokeVoidAsync("MapOLAddLayer", _mapId, newLayer.InternalLayer); + } }); } @@ -654,13 +661,48 @@ private void ShapesOnCollectionChanged(object? sender, NotifyCollectionChangedEv Task.Run(async () => { - if (e.OldItems != null) - foreach (var oldShape in e.OldItems.OfType()) - await _module.InvokeVoidAsync("MapOLRemoveShape", _mapId, oldShape.InternalFeature); + if (e.Action == NotifyCollectionChangedAction.Reset) + { + await _module.InvokeVoidAsync("MapOLSetShapes", _mapId, null); + } + else + { + if (e.OldItems != null) + foreach (var oldShape in e.OldItems.OfType()) + await _module.InvokeVoidAsync("MapOLRemoveShape", _mapId, oldShape.InternalFeature); + + if (e.NewItems != null) + foreach (var newShape in e.NewItems.OfType()) + await _module.InvokeVoidAsync("MapOLAddShape", _mapId, newShape.InternalFeature); + } + }); + } + + private void MarkersOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (_module == null) + return; + + if (e.NewItems != null) + foreach (var newShape in e.NewItems.OfType()) + newShape.ParentMap = this; + + Task.Run(async () => + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + await _module.InvokeVoidAsync("MapOLMarkers", _mapId, null); + } + else + { + if (e.OldItems != null) + foreach (var oldShape in e.OldItems.OfType()) + await _module.InvokeVoidAsync("MapOLRemoveShape", _mapId, oldShape.InternalFeature); - if (e.NewItems != null) - foreach (var newShape in e.NewItems.OfType()) - await _module.InvokeVoidAsync("MapOLAddShape", _mapId, newShape.InternalFeature); + if (e.NewItems != null) + foreach (var newShape in e.NewItems.OfType()) + await _module.InvokeVoidAsync("MapOLAddShape", _mapId, newShape.InternalFeature); + } }); } } \ No newline at end of file diff --git a/src/OpenLayers.Blazor/Marker.cs b/src/OpenLayers.Blazor/Marker.cs index a2c0f02..c2f22a6 100644 --- a/src/OpenLayers.Blazor/Marker.cs +++ b/src/OpenLayers.Blazor/Marker.cs @@ -2,12 +2,24 @@ namespace OpenLayers.Blazor; +/// +/// A Marker component to attach to a +/// public class Marker : Shape { + /// + /// Default Constructor + /// public Marker() : base(new Internal.Marker()) { } + /// + /// Constructor with key parameters. + /// + /// + /// + /// public Marker(MarkerType type, Coordinate coordinate, string? title = null) : this() { Type = type; @@ -15,6 +27,11 @@ public Marker(MarkerType type, Coordinate coordinate, string? title = null) : th Title = title; } + /// + /// Constructor for a Marker + /// + /// + /// public Marker(Coordinate coordinate, char icon) : this() { Type = MarkerType.MarkerAwesome; @@ -22,6 +39,15 @@ public Marker(Coordinate coordinate, char icon) : this() Label = icon.ToString(); } + /// + /// Extended consturctor. + /// + /// + /// + /// + /// + /// + /// public Marker(Coordinate coordinate, string imageSource, float width, float height, float anchorX, float anchorY) : this() { Type = MarkerType.MarkerCustomImage; @@ -31,6 +57,9 @@ public Marker(Coordinate coordinate, string imageSource, float width, float heig InternalFeature.Anchor = new[] { anchorX, anchorY }; } + /// + /// Type of the marker: + /// [Parameter] public MarkerType Type { @@ -38,6 +67,9 @@ public MarkerType Type set => InternalFeature.Style = value.ToString(); } + /// + /// Size of the marker in width/height + /// [Parameter] public float[]? Size { @@ -45,6 +77,9 @@ public float[]? Size set => InternalFeature.Size = value; } + /// + /// Anchor point for the marker image/icon + /// [Parameter] public float[]? Anchor { @@ -53,10 +88,23 @@ public float[]? Anchor } + /// + /// Coordinate of the marker. + /// [Parameter] public Coordinate? Coordinate { get => InternalFeature.Point; set => InternalFeature.Point = value?.Value; } + + /// + /// Icon Rotation in radiant + /// + [Parameter] + public double? Rotation + { + get => InternalFeature.Rotation; + set => InternalFeature.Rotation = value; + } } \ No newline at end of file diff --git a/src/OpenLayers.Blazor/OpenStreetMap.cs b/src/OpenLayers.Blazor/OpenStreetMap.cs index 14ce21f..aab4110 100644 --- a/src/OpenLayers.Blazor/OpenStreetMap.cs +++ b/src/OpenLayers.Blazor/OpenStreetMap.cs @@ -9,6 +9,5 @@ public OpenStreetMap() SourceType = SourceType.OSM }); Center = new Coordinate(0, 0); - Zoom = 10; } } \ No newline at end of file diff --git a/src/OpenLayers.Blazor/SwissMap.cs b/src/OpenLayers.Blazor/SwissMap.cs index c5ae4b4..8aad75f 100644 --- a/src/OpenLayers.Blazor/SwissMap.cs +++ b/src/OpenLayers.Blazor/SwissMap.cs @@ -9,8 +9,8 @@ public SwissMap() LayerId = "ch.swisstopo.pixelkarte-farbe"; SetBaseLayer(LayerId); Center = new Coordinate { X = 2660013.54, Y = 1185171.98 }; // Swiss Center - Zoom = 2.4; Defaults.CoordinatesProjection = "EPSG:2056"; // VT95 + Zoom = 2.4; } diff --git a/src/OpenLayers.Blazor/wwwroot/openlayers_interop.js b/src/OpenLayers.Blazor/wwwroot/openlayers_interop.js index 0564303..c11d2f0 100644 --- a/src/OpenLayers.Blazor/wwwroot/openlayers_interop.js +++ b/src/OpenLayers.Blazor/wwwroot/openlayers_interop.js @@ -900,8 +900,13 @@ MapOL.prototype.onFeatureChanged = function (feature) { MapOL.prototype.updateShape = function (shape) { - var feature = this.Geometries.getSource().getFeatureById(shape.id); - if (!feature && this.GeoLayer) feature = this.GeoLayer.getSource().getFeatureById(shape.id); + var feature; + if (shape.properties.type == "Marker") { + feature = this.Markers.getSource().getFeatureById(shape.id); + } else { + feature = this.Geometries.getSource().getFeatureById(shape.id); + if (!feature && this.GeoLayer) feature = this.GeoLayer.getSource().getFeatureById(shape.id); + } if (feature) { const newFeature = this.mapShapeToFeature(shape); @@ -914,7 +919,7 @@ MapOL.prototype.updateShape = function (shape) { MapOL.prototype.removeShape = function (shape) { var source; - if (shape.type == "Marker") + if (shape.properties.type == "Marker") source = this.Markers.getSource(); else source = this.Geometries.getSource(); @@ -926,7 +931,7 @@ MapOL.prototype.removeShape = function (shape) { MapOL.prototype.addShape = function (shape) { var source; - if (shape.type == "Marker") + if (shape.properties.type == "Marker") source = this.Markers.getSource(); else source = this.Geometries.getSource(); @@ -1076,6 +1081,7 @@ MapOL.prototype.customImageStyle = function (marker) { offset: [0, 0], opacity: 1, scale: marker.scale, + rotation: marker.rotation, anchorXUnits: "pixels", anchorYUnits: "pixels", src: marker.properties["content"]