127 lines
3.8 KiB
TypeScript
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);
|
|
};
|