import * as L from 'leaflet'; /** * Options interface extending standard Circle options * to include custom callback hooks. */ interface ActionCircleOptions extends L.CircleOptions { onAccept: (instance: ActionCircle) => void; onClear: (instance: ActionCircle) => void; } class ActionCircle extends L.Circle { declare options: ActionCircleOptions; private _btnContainer: HTMLDivElement | null = null; constructor(latlng: L.LatLngExpression, options: ActionCircleOptions) { super(latlng, options); } // Called when the circle is added to the map override onAdd(map: L.Map): this { super.onAdd(map); this._initUI(map); // map.on('zoom, zoomend, move, moveend', this._repositionUI, this); // Keep buttons synced with circle position during pan/zoom map.on('zoom, zoomend', this._repositionUI, this); map.on('move, moveend', this._repositionUI, this); map.on('viewreset', this._repositionUI, this); return this; } override setLatLng(latLng: L.LatLngExpression): this { super.setLatLng(latLng); this._repositionUI(); return this; } // Called when circle.remove() or map.removeLayer(circle) is executed override onRemove(map: L.Map): this { if (this._btnContainer) { L.DomUtil.remove(this._btnContainer); this._btnContainer = null; } map.off('zoom, zoomend', this._repositionUI, this); map.off('move, moveend', this._repositionUI, this); map.off('viewreset', this._repositionUI, this); super.onRemove(map); return this; } override setRadius(radius: number): this { super.setRadius(radius); this._repositionUI(); return this; } private _initUI(map: L.Map): void { // Create container in the markerPane (above the circle path) const container = L.DomUtil.create('div', 'leaflet-circle-actions', map.getPane('markerPane')); this._btnContainer = container; // Prevent map clicks/drags from triggering when interacting with buttons L.DomEvent.disableClickPropagation(container); // Accept Button const acceptBtn = L.DomUtil.create('button', 'circle-btn-accept', container); L.DomUtil.create('i', 'q-icon mdi mdi-check', acceptBtn); L.DomEvent.on(acceptBtn, 'click', (e) => { L.DomEvent.stopPropagation(e); if (this.options.onAccept) this.options.onAccept(this); }); // Clear Button const clearBtn = L.DomUtil.create('button', 'circle-btn-clear', container); L.DomUtil.create('i', 'q-icon mdi mdi-close', clearBtn); L.DomEvent.on(clearBtn, 'click', (e) => { L.DomEvent.stopPropagation(e); if (this.options.onClear) this.options.onClear(this); this.remove(); // Self-destruct }); this._repositionUI(); } private readonly _repositionUI = (): void => { if (!this._btnContainer || !this._map) return; const bounds = this.getBounds(); const southEast = bounds.getSouthEast(); const pixelPoint = this._map.latLngToLayerPoint(southEast); const radius = this.getRadius(); const radiusRat = radius / 1000; const translate = `translate(${radiusRat}, ${radiusRat})`; /* if (radius < 30) { translate = 'translate(25%, 25%)'; } if (radius > 200) { translate = 'translate(75%, 75%)'; } */ // Convert LatLng center to pixel coordinates // const centerPoint = this._map.latLngToLayerPoint(this.getLatLng()); // Apply styles to overlay the center Object.assign(this._btnContainer.style, { position: 'absolute', left: `${pixelPoint.x}px`, top: `${pixelPoint.y}px`, zIndex: '1000', display: 'flex', gap: '8px', transform: translate, // Centering trick pointerEvents: 'auto', }); }; } // Factory function for cleaner initialization export const actionCircle = (latlng: L.LatLngExpression, options: ActionCircleOptions) => { return new ActionCircle(latlng, options); };