Files
sim-location-frontend/src/components/leaflet/actionCircle.ts
T
2026-06-10 11:00:10 -04:00

127 lines
3.8 KiB
TypeScript

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);
};