From c4d77d5b15eb6d1f34d4ea09bdef9695c9f044d4 Mon Sep 17 00:00:00 2001 From: Arnaud Leclerc Date: Tue, 10 Mar 2026 18:38:43 +0100 Subject: [PATCH] Releases/1.18.0 (#119) * Add ability to add and remove shapes from drawing manager data source (#110) * Add Drawing Manager component that can be used to add or remove shapes from drawing manager datasource * Add sample page for adding and removing shapes. * Add tests for DrawingManager. * Upgrade projects to supported .NET versions (net8.0, net9.0, net10.0) (#115) - Drop net6.0 (EOL Nov 2024) and net7.0 (EOL May 2024) - Add net9.0 and net10.0 target frameworks to library and test projects - Update sample project to target net10.0 - Add per-TFM package references for net9.0 and net10.0 - Upgrade bunit from 1.26.64 to 1.37.7 for .NET 9/10 compatibility - Update all CI workflows to set up .NET 8.0, 9.0, and 10.0 SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enable NuGet publish step in release workflow (#116) Re-enable the commented-out dotnet nuget push step in release.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trigger release workflow on changes to itself (#117) Add .github/workflows/release.yml to the paths filter so the workflow also runs when the release workflow file itself is modified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix missing static web assets in NuGet package (#118) The Razor SDK generates the static web assets manifest at dotnet build time. Previously, npm run build (which outputs the JS files to wwwroot/) ran after dotnet build, so the manifest was generated with an empty wwwroot/ and dotnet pack --no-build reused that stale manifest. Fix by moving the npm install/lint/build steps before dotnet restore and dotnet build, ensuring the compiled JS files exist in wwwroot/ when the static web assets manifest is generated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unused `_sourceShapes` field from `DrawingManager` (#121) * Initial plan * Remove unused _sourceShapes field from DrawingManager to reduce memory overhead Co-authored-by: arnaudleclerc <9578038+arnaudleclerc@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arnaudleclerc <9578038+arnaudleclerc@users.noreply.github.com> --------- Co-authored-by: mtech-rherpio <166818039+mtech-rherpio@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: arnaudleclerc <9578038+arnaudleclerc@users.noreply.github.com> --- .github/workflows/build.yml | 12 +- .github/workflows/code-coverage.yml | 12 +- .github/workflows/release.yml | 37 ++- .github/workflows/unit-tests.yml | 12 +- .../AzureMapsControl.Sample.csproj | 2 +- .../Components/Layout/NavMenu.razor | 3 + .../Drawing/DrawingManagerLoadData.razor | 103 +++++++ .../AzureMapsControl.Components.csproj | 20 +- .../Drawing/DrawingManager.cs | 138 +++++++++ src/AzureMapsControl.Components/Map/Map.cs | 8 + .../typescript/drawing/drawing.ts | 11 + .../AzureMapsControl.Components.Tests.csproj | 4 +- .../Drawing/DrawingManager.cs | 285 ++++++++++++++++++ 13 files changed, 596 insertions(+), 51 deletions(-) create mode 100644 samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor create mode 100644 src/AzureMapsControl.Components/Drawing/DrawingManager.cs create mode 100644 tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8adb68..40dce13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,18 +21,18 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET 6.0 + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x - - name: Setup .NET 7.0 + dotnet-version: 8.0.x + - name: Setup .NET 9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x - - name: Setup .NET 8.0 + dotnet-version: 9.0.x + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj - name: Build diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 123dac1..28a7edd 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,18 +27,18 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET 6.0 + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x - - name: Setup .NET 7.0 + dotnet-version: 8.0.x + - name: Setup .NET 9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x - - name: Setup .NET 8.0 + dotnet-version: 9.0.x + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b794892..d1bf899 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ on: - main paths: - src/** + - .github/workflows/release.yml jobs: build: @@ -20,38 +21,34 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - name: Setup .NET 7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x - name: Setup .NET 8.0 uses: actions/setup-dotnet@v1 with: dotnet-version: 8.0.x - - name: Setup .NET Core 3.1 + - name: Setup .NET 9.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 10.0.x - name: Install GitVersion uses: gittools/actions/gitversion/setup@v0.9.11 with: versionSpec: '5.x' - - name: Install dependencies - run: dotnet restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj - - name: Build - run: dotnet build --configuration Release --no-restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj - - name: Test - run: dotnet test ./tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj - name: NPM Install run: cd ./src/AzureMapsControl.Components && npm i - name: Lint run: cd ./src/AzureMapsControl.Components && npm run lint - name: Build Typescript run: cd ./src/AzureMapsControl.Components && npm run build + - name: Install dependencies + run: dotnet restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj + - name: Build + run: dotnet build --configuration Release --no-restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj + - name: Test + run: dotnet test ./tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj - name: Use GitVersion id: gitversion # step id used as reference for output values uses: gittools/actions/gitversion/execute@v0.9.11 @@ -106,7 +103,7 @@ jobs: - name: Pack if: ${{ steps.gitversion.outputs.branchName == 'master' || steps.gitversion.outputs.branchName == 'main' }} run: dotnet pack ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj -p:Version='${{ steps.gitversion.outputs.majorMinorPatch }}"' -c Release --no-build - # - name: Publish - # env : - # NUGETAPIKEY: ${{secrets.NUGETAPIKEY}} - # run: dotnet nuget push "**/AzureMapsControl.Components*.nupkg" -s https://api.nuget.org/v3/index.json -k $NUGETAPIKEY --skip-duplicate \ No newline at end of file + - name: Publish + env: + NUGETAPIKEY: ${{secrets.NUGETAPIKEY}} + run: dotnet nuget push "**/AzureMapsControl.Components*.nupkg" -s https://api.nuget.org/v3/index.json -k $NUGETAPIKEY --skip-duplicate \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 678e17e..97f57a9 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -26,18 +26,18 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET 6.0 + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x - - name: Setup .NET 7.0 + dotnet-version: 8.0.x + - name: Setup .NET 9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x - - name: Setup .NET 8.0 + dotnet-version: 9.0.x + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore ./src/AzureMapsControl.Components/AzureMapsControl.Components.csproj - name: Build diff --git a/samples/AzureMapsControl.Sample/AzureMapsControl.Sample.csproj b/samples/AzureMapsControl.Sample/AzureMapsControl.Sample.csproj index 8768bb9..182c9e5 100644 --- a/samples/AzureMapsControl.Sample/AzureMapsControl.Sample.csproj +++ b/samples/AzureMapsControl.Sample/AzureMapsControl.Sample.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor b/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor index 4f33778..4a0e462 100644 --- a/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor +++ b/samples/AzureMapsControl.Sample/Components/Layout/NavMenu.razor @@ -86,6 +86,9 @@
  • Toolbar update
  • +
  • + Load data to drawing manager +
  • Indoor diff --git a/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor b/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor new file mode 100644 index 0000000..6a88084 --- /dev/null +++ b/samples/AzureMapsControl.Sample/Components/Pages/Drawing/DrawingManagerLoadData.razor @@ -0,0 +1,103 @@ +@page "/Drawing/DrawingManagerLoadData" +@rendermode InteractiveServer + +@using AzureMapsControl.Components.Atlas +@using AzureMapsControl.Components.Drawing +@using AzureMapsControl.Components.Map + + + +
    + + +
    + + +@code { + private DrawingManager? _drawingManager; + private Position _center = new Position(-122.33, 47.6); + + public async Task MapReady(MapEventArgs eventArgs) + { + await eventArgs.Map.SetCameraOptionsAsync(options => + { + options.Zoom = 10; + options.Center = _center; + }); + await eventArgs.Map.AddDrawingToolbarAsync(new AzureMapsControl.Components.Drawing.DrawingToolbarOptions + { + Buttons = new[] + { + AzureMapsControl.Components.Drawing.DrawingButton.DrawCircle, + AzureMapsControl.Components.Drawing.DrawingButton.DrawLine, + AzureMapsControl.Components.Drawing.DrawingButton.EditGeometry + }, + Position = AzureMapsControl.Components.Controls.ControlPosition.TopRight, + Style = AzureMapsControl.Components.Drawing.DrawingToolbarStyle.Dark + }); + + var lineString = new AzureMapsControl.Components.Atlas.LineString(new[] + { + new AzureMapsControl.Components.Atlas.Position(-122.27577, 47.55938), + new AzureMapsControl.Components.Atlas.Position(-122.29705, 47.60662), + new AzureMapsControl.Components.Atlas.Position(-122.22358, 47.6367) + }); + var shape = new AzureMapsControl.Components.Atlas.Shape(lineString); + _drawingManager = eventArgs.Map.DrawingManager; + await _drawingManager.AddShapesAsync(new[] { shape }); + } + + private async Task AddRandomShape() + { + if (_drawingManager == null) return; + + var random = new Random(); + var shapeType = random.Next(3); + Shape shape; + var numberOfPoints = random.Next(3, 5); + var center = new Position(_center.Longitude + (random.NextDouble()-0.5) * 0.6, _center.Latitude + (random.NextDouble()-0.5) * 0.4); + + switch (shapeType) + { + case 0: // Circle + var radius = random.NextDouble() * 2000; + shape = new Shape(new Point(center), new Dictionary + { + { "subType", "Circle" }, + { "radius", radius } + }); + break; + case 1: // Polygon + var polygonPositions = new List(); + for (var i = 0; i < numberOfPoints; i++) + { + polygonPositions.Add(new Position(center.Longitude + (random.NextDouble()-0.5) * 0.1, center.Latitude + (random.NextDouble()-0.5) * 0.1)); + } + polygonPositions.Add(polygonPositions[0]); + shape = new Shape(new Polygon(new[] { polygonPositions })); + break; + case 2: // Polyline + var polylinePositions = new List(); + for (var i = 0; i < numberOfPoints; i++) + { + polylinePositions.Add(new Position(center.Longitude + (random.NextDouble()-0.5) * 0.1, center.Latitude + (random.NextDouble()-0.5) * 0.1)); + } + shape = new Shape(new LineString(polylinePositions)); + break; + default: + return; + } + + await _drawingManager.AddShapesAsync(new[] { shape }); + } + + private async Task ClearShapes() + { + if (_drawingManager != null) + { + await _drawingManager.ClearAsync(); + } + } +} \ No newline at end of file diff --git a/src/AzureMapsControl.Components/AzureMapsControl.Components.csproj b/src/AzureMapsControl.Components/AzureMapsControl.Components.csproj index 13850d4..ae0e999 100644 --- a/src/AzureMapsControl.Components/AzureMapsControl.Components.csproj +++ b/src/AzureMapsControl.Components/AzureMapsControl.Components.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0;net8.0 + net8.0;net9.0;net10.0 3.0 Arnaud Leclerc @@ -37,19 +37,19 @@ - - - + + + - - - + + + - - - + + + diff --git a/src/AzureMapsControl.Components/Drawing/DrawingManager.cs b/src/AzureMapsControl.Components/Drawing/DrawingManager.cs new file mode 100644 index 0000000..f94b719 --- /dev/null +++ b/src/AzureMapsControl.Components/Drawing/DrawingManager.cs @@ -0,0 +1,138 @@ +namespace AzureMapsControl.Components.Drawing +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + using AzureMapsControl.Components.Atlas; + using AzureMapsControl.Components.Logger; + using AzureMapsControl.Components.Runtime; + + using Microsoft.Extensions.Logging; + + + /// + /// DrawingManager for the DrawingToolbar + /// + public sealed class DrawingManager + { + internal IMapJsRuntime JSRuntime { get; set; } + internal ILogger Logger { get; set; } + public bool Disposed { get; private set; } + + /// + /// Add shapes to the drawing manager data source + /// + /// Shapes to add + /// + /// The control has not been added to the map + /// The control has already been disposed + public async ValueTask AddShapesAsync(IEnumerable shapes) + { + if (shapes == null || !shapes.Any()) + { + return; + } + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + var lineStrings = shapes.OfType>(); + if (lineStrings.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{lineStrings.Count()} linestrings will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), lineStrings); + } + + var multiLineStrings = shapes.OfType>(); + if (multiLineStrings.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiLineStrings.Count()} multilinestrings will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiLineStrings); + } + + var multiPoints = shapes.OfType>(); + if (multiPoints.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiPoints.Count()} multipoints will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiPoints); + } + + var multiPolygons = shapes.OfType>(); + if (multiPolygons.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{multiPolygons.Count()} multipolygons will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), multiPolygons); + } + + var points = shapes.OfType>(); + if (points.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{points.Count()} points will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), points); + } + + var polygons = shapes.OfType>(); + if (polygons.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{polygons.Count()} polygons will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), polygons); + } + + var routePoints = shapes.OfType>(); + if (routePoints.Any()) + { + Logger?.LogAzureMapsControlDebug(AzureMapLogEvent.Source_AddAsync, $"{routePoints.Count()} route points will be added"); + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), routePoints); + } + } + + /// + /// Clear the drawing manager source + /// + /// + /// The control has not been added to the map + /// The control has already been disposed + public async ValueTask ClearAsync() + { + Logger?.LogAzureMapsControlInfo(AzureMapLogEvent.Source_ClearAsync, "Clearing drawing manager source"); + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + await JSRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()); + } + + /// + /// Mark the control as disposed + /// + /// + /// The control has not been added to the map + /// The control has already been disposed + internal void Dispose() + { + Logger?.LogAzureMapsControlInfo(AzureMapLogEvent.Source_DisposeAsync, "DrawingManager - Dispose"); + + EnsureJsRuntimeExists(); + EnsureNotDisposed(); + + Disposed = true; + } + + private void EnsureJsRuntimeExists() + { + if (JSRuntime is null) + { + throw new Exceptions.ComponentNotAddedToMapException(); + } + } + + private void EnsureNotDisposed() + { + if (Disposed) + { + throw new Exceptions.ComponentDisposedException(); + } + } + } +} diff --git a/src/AzureMapsControl.Components/Map/Map.cs b/src/AzureMapsControl.Components/Map/Map.cs index af082c8..5fa2910 100644 --- a/src/AzureMapsControl.Components/Map/Map.cs +++ b/src/AzureMapsControl.Components/Map/Map.cs @@ -67,6 +67,8 @@ public sealed class Map public DrawingToolbarOptions DrawingToolbarOptions { get; internal set; } + public DrawingManager DrawingManager { get; internal set; } + public IEnumerable Controls => _controls; public IEnumerable Layers => _layers; @@ -352,6 +354,10 @@ await _jsRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Drawing.AddDrawin Events = drawingToolbarOptions.Events?.EnabledEvents }, DotNetObjectReference.Create(_drawingToolbarEventInvokeHelper)); + DrawingManager = new DrawingManager() { + JSRuntime = _jsRuntime, + Logger = _logger + }; } } @@ -394,6 +400,8 @@ public async ValueTask RemoveDrawingToolbarAsync() { await _jsRuntime.InvokeVoidAsync(Constants.JsConstants.Methods.Drawing.RemoveDrawingToolbar.ToDrawingNamespace()); DrawingToolbarOptions = null; + DrawingManager?.Dispose(); + DrawingManager = null; } } diff --git a/src/AzureMapsControl.Components/typescript/drawing/drawing.ts b/src/AzureMapsControl.Components/typescript/drawing/drawing.ts index 4959cac..69120bf 100644 --- a/src/AzureMapsControl.Components/typescript/drawing/drawing.ts +++ b/src/AzureMapsControl.Components/typescript/drawing/drawing.ts @@ -3,6 +3,8 @@ import * as azmaps from 'azure-maps-control'; import { EventHelper } from '../events/event-helper'; import { Core } from '../core/core'; import { DrawingEventArgs } from './drawing-event-args'; +import { Shape } from '../geometries/geometry'; +import { GeometryBuilder } from '../geometries/geometry-builder'; export class Drawing { @@ -96,4 +98,13 @@ export class Drawing { }); } + public static addShapes(shapes: Shape[]): void { + const mapsShapes = shapes.map(shape => GeometryBuilder.buildShape(shape)); + this._drawingManager.getSource().add(mapsShapes); + } + + public static clear(): void { + this._drawingManager.getSource().clear(); + } + } \ No newline at end of file diff --git a/tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj b/tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj index d5c783e..cfa3b33 100644 --- a/tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj +++ b/tests/AzureMapsControl.Components.Tests/AzureMapsControl.Components.Tests.csproj @@ -1,12 +1,12 @@  - net6.0;net7.0;net8.0 + net8.0;net9.0;net10.0 false - + diff --git a/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs b/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs new file mode 100644 index 0000000..3eaccf9 --- /dev/null +++ b/tests/AzureMapsControl.Components.Tests/Drawing/DrawingManager.cs @@ -0,0 +1,285 @@ +namespace AzureMapsControl.Components.Tests.Drawing +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + using AzureMapsControl.Components.Atlas; + using AzureMapsControl.Components.Drawing; + using AzureMapsControl.Components.Exceptions; + using AzureMapsControl.Components.Runtime; + + using Microsoft.Extensions.Logging; + + using Moq; + + using Xunit; + + public class DrawingManagerTests + { + private readonly Mock _jsRuntimeMock = new(); + private readonly Mock _loggerMock = new(); + + [Fact] + public void Should_HaveDefaultProperties() + { + var drawingManager = new DrawingManager(); + + Assert.False(drawingManager.Disposed); + Assert.Null(drawingManager.JSRuntime); + Assert.Null(drawingManager.Logger); + } + + [Fact] + public void Should_SetJSRuntimeAndLogger() + { + var drawingManager = CreateInitializedDrawingManager(); + + Assert.Equal(_jsRuntimeMock.Object, drawingManager.JSRuntime); + Assert.Equal(_loggerMock.Object, drawingManager.Logger); + } + + [Fact] + public async Task Should_AddShapes_AllSupportedGeometryTypes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new LineString()), + new Shape(new MultiLineString()), + new Shape(new MultiPoint()), + new Shape(new MultiPolygon()), + new Shape(new Polygon()), + new Shape(new RoutePoint()), + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(7)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_AddMultipleShapesOfSameType_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new Point()), + new Shape(new Point()) + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 3)), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_HandleMixedGeometryTypes_InSingleCall_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var shapes = new List + { + new Shape(new Point()), + new Shape(new Point()), + new Shape(new LineString()), + new Shape(new Polygon()), + new Shape(new Polygon()), + new Shape(new Polygon()) + }; + + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 2)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 3)), Times.Once); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(3)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_HandleLargeNumberOfShapes_Efficiently_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + // Create a large collection of shapes + var shapes = new List(); + for (int i = 0; i < 1000; i++) + { + shapes.Add(new Shape(new Point())); + } + + await drawingManager.AddShapesAsync(shapes); + + // Should still only make one call per geometry type + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.Is>>(s => s.Count() == 1000)), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_NotAddShapes_WhenNull_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.AddShapesAsync(null); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_NotAddShapes_WhenEmpty_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.AddShapesAsync(new List()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ClearShapes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + await drawingManager.ClearAsync(); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_AccumulateShapes_InInternalState_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + var firstBatch = new List { new Shape(new Point()) }; + var secondBatch = new List { new Shape(new LineString()) }; + var thirdBatch = new List { new Shape(new Point()) }; + + await drawingManager.AddShapesAsync(firstBatch); + await drawingManager.AddShapesAsync(secondBatch); + await drawingManager.AddShapesAsync(thirdBatch); + + // Verify correct number of JS calls + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync( + Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), + It.IsAny()), Times.Exactly(3)); // 1 Point call + 1 LineString call + 1 more Point call + } + + [Fact] + public async Task Should_ClearInternalState_WhenCleared_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + var shapes = new List { new Shape(new Point()) }; + + // Add shapes then clear + await drawingManager.AddShapesAsync(shapes); + await drawingManager.ClearAsync(); + + // Adding again should work without errors + await drawingManager.AddShapesAsync(shapes); + + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.AddShapes.ToDrawingNamespace(), It.IsAny()), Times.Exactly(2)); + _jsRuntimeMock.Verify(runtime => runtime.InvokeVoidAsync(Constants.JsConstants.Methods.Source.Clear.ToDrawingNamespace()), Times.Once); + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_Dispose_Successfully() + { + var drawingManager = CreateInitializedDrawingManager(); + + Assert.False(drawingManager.Disposed); + + drawingManager.Dispose(); + + Assert.True(drawingManager.Disposed); + } + + [Fact] + public async Task Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_Clear_Async() + { + var drawingManager = new DrawingManager(); + + await Assert.ThrowsAsync(async () => await drawingManager.ClearAsync()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_AddShapes_Async() + { + var drawingManager = new DrawingManager(); + var shapes = new List { new Shape(new Point()) }; + + await Assert.ThrowsAsync(async () => await drawingManager.AddShapesAsync(shapes)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_ThrowComponentNotAddedToMapException_WhenJSRuntimeIsNull_Dispose() + { + var drawingManager = new DrawingManager(); + + Assert.Throws(() => drawingManager.Dispose()); + } + + [Fact] + public async Task Should_ThrowComponentDisposedException_WhenDisposed_Clear_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + drawingManager.Dispose(); + + await Assert.ThrowsAsync(async () => await drawingManager.ClearAsync()); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_ThrowComponentDisposedException_WhenDisposed_AddShapes_Async() + { + var drawingManager = CreateInitializedDrawingManager(); + + drawingManager.Dispose(); + var shapes = new List { new Shape(new Point()) }; + + await Assert.ThrowsAsync(async () => await drawingManager.AddShapesAsync(shapes)); + + _jsRuntimeMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_ThrowComponentDisposedException_WhenAlreadyDisposed_Dispose() + { + var drawingManager = CreateInitializedDrawingManager(); + drawingManager.Dispose(); + + Assert.Throws(() => drawingManager.Dispose()); + } + + private DrawingManager CreateInitializedDrawingManager() + { + return new DrawingManager() { + JSRuntime = _jsRuntimeMock.Object, + Logger = _loggerMock.Object + }; + } + } +}