From 26212e0e31e43aa4a2a127c17cf0715898771286 Mon Sep 17 00:00:00 2001 From: Jens Ahremark Date: Thu, 27 Nov 2025 15:52:13 +0100 Subject: [PATCH 1/4] Add call to get geometries from terradraw --- Community.Blazor.MapLibre/MapLibre.razor.cs | 10 +++++++++- Community.Blazor.MapLibre/MapLibre.razor.js | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Community.Blazor.MapLibre/MapLibre.razor.cs b/Community.Blazor.MapLibre/MapLibre.razor.cs index 9a223f5..72d2cc6 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.cs +++ b/Community.Blazor.MapLibre/MapLibre.razor.cs @@ -259,7 +259,15 @@ public async Task FinishGeometryAsync() } await _jsModule.InvokeVoidAsync("finishGeometry", MapId); } - + /// + /// Queries the map for rendered features within a specified geometry or options. + /// + /// An array of features. + public async ValueTask GetTerraDrawGeometriesAsync() + { + return await _jsModule.InvokeAsync("getTerraDrawGeometries", MapId); + } + [JSInvokable] public Task OnTerraDrawReady() => Task.CompletedTask; [JSInvokable] public Task OnTerraDrawChanged(string geoJson) => Task.CompletedTask; diff --git a/Community.Blazor.MapLibre/MapLibre.razor.js b/Community.Blazor.MapLibre/MapLibre.razor.js index b3b2df2..8005a70 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.js +++ b/Community.Blazor.MapLibre/MapLibre.razor.js @@ -226,6 +226,18 @@ export function finishGeometry(container) { element.dispatchEvent(event); } +/** + * Get created geometries from terra-draw. + * + * @param {string} container - The identifier of the map container. + * @returns {Array} geometries - The created geometries. + */ +export function getTerraDrawGeometries(container) +{ + const draw = drawControls[container]; + return draw.getSnapshot(); +} + /** * Adds a scale control to the given map container. * From cd10ce8de1524aca33c8d026ff14675d971749eb Mon Sep 17 00:00:00 2001 From: Lars Arvidsson Date: Mon, 8 Dec 2025 20:54:40 +0100 Subject: [PATCH 2/4] GEOIN-79 Added functions for listening on some terra draw events --- Community.Blazor.MapLibre/MapLibre.razor.cs | 23 ++++++++++++++++++++ Community.Blazor.MapLibre/MapLibre.razor.js | 24 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Community.Blazor.MapLibre/MapLibre.razor.cs b/Community.Blazor.MapLibre/MapLibre.razor.cs index 72d2cc6..544f93e 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.cs +++ b/Community.Blazor.MapLibre/MapLibre.razor.cs @@ -270,6 +270,29 @@ public async ValueTask GetTerraDrawGeometriesAsync() [JSInvokable] public Task OnTerraDrawReady() => Task.CompletedTask; [JSInvokable] public Task OnTerraDrawChanged(string geoJson) => Task.CompletedTask; + + + public async Task AddTerraDrawFinishListener(Action handler) + { + var callback = new CallbackHandler(_jsModule, "finish", handler, typeof(T)); + var reference = DotNetObjectReference.Create(callback); + _references.TryAdd(Guid.NewGuid(), reference); + + await _jsModule.InvokeVoidAsync("onTerraDrawFinish", MapId, reference); + + return new Listener(callback); + } + + public async Task AddTerraDrawDeleteListener(Action handler) + { + var callback = new CallbackHandler(_jsModule, "delete", handler, typeof(T)); + var reference = DotNetObjectReference.Create(callback); + _references.TryAdd(Guid.NewGuid(), reference); + + await _jsModule.InvokeVoidAsync("onTerraDrawDelete", MapId, reference); + + return new Listener(callback); + } /// /// Adds a geolocate control to the given map container. diff --git a/Community.Blazor.MapLibre/MapLibre.razor.js b/Community.Blazor.MapLibre/MapLibre.razor.js index 8005a70..0a1c85e 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.js +++ b/Community.Blazor.MapLibre/MapLibre.razor.js @@ -238,6 +238,30 @@ export function getTerraDrawGeometries(container) return draw.getSnapshot(); } +export function onTerraDrawFinish(container, dotnetReference) { + const draw = drawControls[container]; + + draw.on("finish", (id, context) => { + if (context.action === "draw" || context.action === "dragCoordinate") { + const features = draw.getSnapshot(); + const featuresAsJson = JSON.stringify(features); + dotnetReference.invokeMethodAsync('Invoke', featuresAsJson); + } + }); +} + +export function onTerraDrawDelete(container, dotnetReference) { + const draw = drawControls[container]; + + draw.on("change", (ids, type) => { + if (type === "delete") { + const features = draw.getSnapshot(); + const featuresAsJson = JSON.stringify(features); + dotnetReference.invokeMethodAsync('Invoke', featuresAsJson); + } + }); +} + /** * Adds a scale control to the given map container. * From 8f808238ddd108930147fff6a09c6ab46dd135b0 Mon Sep 17 00:00:00 2001 From: Lars Arvidsson Date: Tue, 9 Dec 2025 11:01:50 +0100 Subject: [PATCH 3/4] GEOIN-79 Removed console.log --- Community.Blazor.MapLibre/MapLibre.razor.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Community.Blazor.MapLibre/MapLibre.razor.js b/Community.Blazor.MapLibre/MapLibre.razor.js index 0a1c85e..fd1215c 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.js +++ b/Community.Blazor.MapLibre/MapLibre.razor.js @@ -126,8 +126,6 @@ export function addGeolocateControl(container, options, position) { export function addNavigationControl(container, options, position) { const map = mapInstances[container]; - console.log("addNavigationControl position: " + position); - if (options === undefined || options === null) { map.addControl(new maplibregl.NavigationControl(), position || undefined); } else { From a03a8cffed1cb3551ce19423468f5d87f55b2d26 Mon Sep 17 00:00:00 2001 From: Jens Ahremark Date: Thu, 11 Dec 2025 22:20:30 +0100 Subject: [PATCH 4/4] GEOIN-60 add deletemode to terradraw --- .../Community.Blazor.MapLibre.csproj | 3 + Community.Blazor.MapLibre/MapLibre.razor.js | 3 +- .../dist/TerraDrawCoordinateDeleteMode.js | 424 ++++++++++++++++++ 3 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 Community.Blazor.MapLibre/wwwroot/terra-draw/dist/TerraDrawCoordinateDeleteMode.js diff --git a/Community.Blazor.MapLibre/Community.Blazor.MapLibre.csproj b/Community.Blazor.MapLibre/Community.Blazor.MapLibre.csproj index f45a17e..283ab00 100644 --- a/Community.Blazor.MapLibre/Community.Blazor.MapLibre.csproj +++ b/Community.Blazor.MapLibre/Community.Blazor.MapLibre.csproj @@ -35,6 +35,9 @@ + + Content + diff --git a/Community.Blazor.MapLibre/MapLibre.razor.js b/Community.Blazor.MapLibre/MapLibre.razor.js index fd1215c..cb361ce 100644 --- a/Community.Blazor.MapLibre/MapLibre.razor.js +++ b/Community.Blazor.MapLibre/MapLibre.razor.js @@ -185,7 +185,8 @@ export function addTerraDrawTool(container, options) { return { valid: true } } }) - drawControls[container] = new terraDraw.TerraDraw({adapter: adapter, modes: [new terraDraw.TerraDrawFreehandMode(), polygonMode, new terraDraw.TerraDrawLineStringMode(), select, new terraDraw.TerraDrawPointMode()]}) + const deleteMode = new TerraDrawCoordinateDeleteModeUmd(); + drawControls[container] = new terraDraw.TerraDraw({adapter: adapter, modes: [new terraDraw.TerraDrawFreehandMode(), polygonMode, new terraDraw.TerraDrawLineStringMode(), select, new terraDraw.TerraDrawPointMode(), deleteMode]}) } /** diff --git a/Community.Blazor.MapLibre/wwwroot/terra-draw/dist/TerraDrawCoordinateDeleteMode.js b/Community.Blazor.MapLibre/wwwroot/terra-draw/dist/TerraDrawCoordinateDeleteMode.js new file mode 100644 index 0000000..64d784e --- /dev/null +++ b/Community.Blazor.MapLibre/wwwroot/terra-draw/dist/TerraDrawCoordinateDeleteMode.js @@ -0,0 +1,424 @@ +/** + * TerraDrawCoordinateDeleteMode v1.0.0 + * Custom TerraDraw mode for deleting coordinates by clicking/tapping + * + * Usage: + * + * + * + * const deleteMode = new TerraDrawCoordinateDeleteMode({ ... }); + */ +var TerraDrawCoordinateDeleteModeUmd = (function () { + 'use strict'; + + function TerraDrawCoordinateDeleteMode(options) { + options = options || {}; + + this._mode = 'delete'; + this._state = 'unregistered'; + this._pointerDistance = options.pointerDistance || 40; + + this._styles = { + deletePointColor: '#DC2626', + deletePointWidth: 6, + highlightColor: '#FCA5A5', + featureOutlineColor: '#EF4444', + featureOutlineWidth: 3, + featureFillColor: '#FEE2E2', + featureFillOpacity: 0.25, + lineStringColor: '#EF4444', + lineStringWidth: 3, + pointColor: '#EF4444', + pointWidth: 10, + pointOutlineColor: '#7F1D1D', + pointOutlineWidth: 2 + }; + + if (options.styles) { + for (var key in options.styles) { + if (options.styles.hasOwnProperty(key)) { + this._styles[key] = options.styles[key]; + } + } + } + + this._deletePoints = []; + this._hoveredDeletePointId = null; + this._hoveredFeatureId = null; + this._onCoordinateDeleted = options.onCoordinateDeleted || null; + this._onFeatureDeleted = options.onFeatureDeleted || null; + + console.log('[TerraDrawCoordinateDeleteMode] Initialized'); + } + + Object.defineProperty(TerraDrawCoordinateDeleteMode.prototype, 'mode', { + get: function() { return this._mode; }, + set: function(value) { this._mode = value; } + }); + + TerraDrawCoordinateDeleteMode.prototype.register = function(config) { + this._store = config.store; + this._project = config.project; + this._unproject = config.unproject; + this._setCursor = config.setCursor; + this._onChange = config.onChange; + this._onFinish = config.onFinish; + this._state = 'registered'; + console.log('[TerraDrawCoordinateDeleteMode] Registered with TerraDraw'); + }; + + TerraDrawCoordinateDeleteMode.prototype.start = function() { + this._state = 'started'; + this._setCursor('pointer'); + this._createDeletePoints(); + console.log('[TerraDrawCoordinateDeleteMode] Started - created', this._deletePoints.length, 'delete points'); + }; + + + TerraDrawCoordinateDeleteMode.prototype.stop = function() { + this._state = 'stopped'; + this._clearDeletePoints(); + this._setCursor('default'); + console.log('[TerraDrawCoordinateDeleteMode] Stopped'); + }; + + TerraDrawCoordinateDeleteMode.prototype.cleanUp = function() { + this._clearDeletePoints(); + }; + + TerraDrawCoordinateDeleteMode.prototype._pixelDistance = function(coord1, coord2) { + var p1 = this._project(coord1[0], coord1[1]); + var p2 = this._project(coord2[0], coord2[1]); + return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); + }; + + TerraDrawCoordinateDeleteMode.prototype._getCoordinatesWithIndices = function(geometry) { + var coords = []; + var i, j; + + if (geometry.type === 'Point') { + coords.push({ + coord: geometry.coordinates, + ringIndex: null, + coordIndex: 0, + isClosingPoint: false + }); + } else if (geometry.type === 'LineString') { + for (i = 0; i < geometry.coordinates.length; i++) { + coords.push({ + coord: geometry.coordinates[i], + ringIndex: null, + coordIndex: i, + isClosingPoint: false + }); + } + } else if (geometry.type === 'Polygon') { + for (i = 0; i < geometry.coordinates.length; i++) { + var ring = geometry.coordinates[i]; + for (j = 0; j < ring.length; j++) { + coords.push({ + coord: ring[j], + ringIndex: i, + coordIndex: j, + isClosingPoint: j === ring.length - 1 + }); + } + } + } + + return coords; + }; + + TerraDrawCoordinateDeleteMode.prototype._createDeletePoints = function() { + var self = this; + this._clearDeletePoints(); + + var features = this._store.copyAll(); + + features.forEach(function(feature) { + if (feature.properties.isDeletePoint || + feature.properties.midPoint || + feature.properties.selectionPoint) { + return; + } + + if (!feature.geometry || + ['Point', 'LineString', 'Polygon'].indexOf(feature.geometry.type) === -1) { + return; + } + + var coordsWithIndices = self._getCoordinatesWithIndices(feature.geometry); + + coordsWithIndices.forEach(function(coordInfo) { + + if (coordInfo.isClosingPoint) return; + + var ringPart = coordInfo.ringIndex !== null ? coordInfo.ringIndex : 'null'; + var deletePointId = 'delete-point-' + feature.id + '-' + ringPart + '-' + coordInfo.coordIndex; + + self._deletePoints.push({ + id: deletePointId, + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coordInfo.coord + }, + properties: { + mode: self._mode, + isDeletePoint: true, + parentFeatureId: feature.id, + ringIndex: coordInfo.ringIndex, + coordIndex: coordInfo.coordIndex, + parentGeometryType: feature.geometry.type + } + }); + }); + }); + + if (this._deletePoints.length > 0) { + this._store.create(this._deletePoints); + } + }; + + TerraDrawCoordinateDeleteMode.prototype._clearDeletePoints = function() { + var allFeatures = this._store.copyAll(); + var deletePointIds = []; + + for (var i = 0; i < allFeatures.length; i++) { + if (allFeatures[i].properties && allFeatures[i].properties.isDeletePoint) { + deletePointIds.push(allFeatures[i].id); + } + } + + if (deletePointIds.length > 0) { + try { + this._store.delete(deletePointIds); + } catch (e) { + console.error('[TerraDrawCoordinateDeleteMode] Failed to delete points:', e); + } + } + + this._deletePoints = []; + this._hoveredDeletePointId = null; + }; + + + TerraDrawCoordinateDeleteMode.prototype._findNearestDeletePoint = function(lng, lat) { + var nearest = null; + var nearestDistance = Infinity; + + for (var i = 0; i < this._deletePoints.length; i++) { + var dp = this._deletePoints[i]; + var distance = this._pixelDistance([lng, lat], dp.geometry.coordinates); + + if (distance < this._pointerDistance && distance < nearestDistance) { + nearest = dp; + nearestDistance = distance; + } + } + + return nearest; + }; + + TerraDrawCoordinateDeleteMode.prototype._deleteCoordinate = function(deletePoint) { + var props = deletePoint.properties; + var parentFeatureId = props.parentFeatureId; + var ringIndex = props.ringIndex; + var coordIndex = props.coordIndex; + var parentGeometryType = props.parentGeometryType; + + var features = this._store.copyAll(); + var parentFeature = null; + for (var i = 0; i < features.length; i++) { + if (features[i].id === parentFeatureId) { + parentFeature = features[i]; + break; + } + } + + if (!parentFeature) return; + + var shouldDeleteFeature = false; + var newGeometry = null; + + if (parentGeometryType === 'Point') { + shouldDeleteFeature = true; + } else if (parentGeometryType === 'LineString') { + var coords = parentFeature.geometry.coordinates.slice(); + + if (coords.length <= 2) { + shouldDeleteFeature = true; + } else { + coords.splice(coordIndex, 1); + newGeometry = { + type: 'LineString', + coordinates: coords + }; + } + } else if (parentGeometryType === 'Polygon') { + var rings = parentFeature.geometry.coordinates.map(function(ring) { + return ring.slice(); + }); + var ring = rings[ringIndex]; + + if (ring.length <= 4) { + if (rings.length === 1 || ringIndex === 0) { + shouldDeleteFeature = true; + } else { + rings.splice(ringIndex, 1); + newGeometry = { + type: 'Polygon', + coordinates: rings + }; + } + } else { + ring.splice(coordIndex, 1); + + // If first point removed, update closing point + if (coordIndex === 0) { + ring[ring.length - 1] = ring[0]; + } + + rings[ringIndex] = ring; + newGeometry = { + type: 'Polygon', + coordinates: rings + }; + } + } + + if (shouldDeleteFeature) { + this._store.delete([parentFeatureId]); + if (this._onFeatureDeleted) { + this._onFeatureDeleted(parentFeature); + } + } else if (newGeometry) { + this._store.updateGeometry([{ + id: parentFeatureId, + geometry: newGeometry + }]); + + if (this._onCoordinateDeleted) { + this._onCoordinateDeleted({ + featureId: parentFeatureId, + ringIndex: ringIndex, + coordIndex: coordIndex, + deletedCoordinate: deletePoint.geometry.coordinates + }); + } + } + + + this._createDeletePoints(); + + if (this._onChange) { + this._onChange([], 'delete'); + } + }; + + TerraDrawCoordinateDeleteMode.prototype.onClick = function(event) { + var nearest = this._findNearestDeletePoint(event.lng, event.lat); + if (nearest) { + this._deleteCoordinate(nearest); + } + }; + + + TerraDrawCoordinateDeleteMode.prototype.onMouseMove = function(event) { + var nearest = this._findNearestDeletePoint(event.lng, event.lat); + + if (nearest) { + if (this._hoveredDeletePointId !== nearest.id) { + this._hoveredDeletePointId = nearest.id; + this._hoveredFeatureId = nearest.properties.parentFeatureId; + this._setCursor('pointer'); + + if (this._onChange) { + this._onChange([], 'styling'); + } + } + } else if (this._hoveredDeletePointId !== null) { + this._hoveredDeletePointId = null; + this._hoveredFeatureId = null; + this._setCursor('crosshair'); + + if (this._onChange) { + this._onChange([], 'styling'); + } + } + }; + + // Required empty handlers + TerraDrawCoordinateDeleteMode.prototype.onKeyDown = function(event) {}; + TerraDrawCoordinateDeleteMode.prototype.onKeyUp = function(event) {}; + TerraDrawCoordinateDeleteMode.prototype.onDragStart = function(event) {}; + TerraDrawCoordinateDeleteMode.prototype.onDrag = function(event) {}; + TerraDrawCoordinateDeleteMode.prototype.onDragEnd = function(event) {}; + + // Style features + TerraDrawCoordinateDeleteMode.prototype.styleFeature = function(feature) { + var styles = this._styles; + var isDeletePoint = feature.properties && feature.properties.isDeletePoint; + var isHoveredDeletePoint = feature.id === this._hoveredDeletePointId; + var isHoveredFeature = feature.id === this._hoveredFeatureId; + + // Style delete point markers + if (isDeletePoint) { + return { + pointColor: isHoveredDeletePoint ? styles.highlightColor : styles.deletePointColor, + pointOutlineColor: styles.deletePointOutlineColor, + pointWidth: isHoveredDeletePoint ? styles.deletePointWidth + 4 : styles.deletePointWidth, + pointOutlineWidth: styles.deletePointOutlineWidth, + zIndex: 1000 + }; + } + + var geomType = feature.geometry ? feature.geometry.type : null; + + if (geomType === 'Point') { + return { + pointColor: isHoveredFeature ? styles.highlightColor : styles.pointColor, + pointOutlineColor: styles.pointOutlineColor, + pointWidth: styles.pointWidth, + pointOutlineWidth: styles.pointOutlineWidth, + zIndex: 10 + }; + } + + if (geomType === 'LineString') { + return { + lineStringColor: isHoveredFeature ? styles.highlightColor : styles.lineStringColor, + lineStringWidth: styles.lineStringWidth, + zIndex: 10 + }; + } + + if (geomType === 'Polygon') { + return { + polygonFillColor: isHoveredFeature ? styles.highlightColor : styles.featureFillColor, + polygonFillOpacity: isHoveredFeature ? 0.4 : styles.featureFillOpacity, + polygonOutlineColor: styles.featureOutlineColor, + polygonOutlineWidth: styles.featureOutlineWidth, + zIndex: 5 + }; + } + + return { + pointColor: styles.pointColor, + lineStringColor: styles.lineStringColor, + polygonFillColor: styles.featureFillColor, + polygonFillOpacity: styles.featureFillOpacity, + polygonOutlineColor: styles.featureOutlineColor, + polygonOutlineWidth: styles.featureOutlineWidth + }; + }; + + // Validate feature + TerraDrawCoordinateDeleteMode.prototype.validateFeature = function(feature) { + return feature.geometry && + ['Point', 'LineString', 'Polygon'].indexOf(feature.geometry.type) !== -1; + }; + + return TerraDrawCoordinateDeleteMode; +})(); +