import type { CrossOrigin, FillRule, LatLng, LatLngBounds, LatLngBoundsExpression, LineCapShape, LineJoinShape, PathOptions, ReferrerPolicy, } from 'leaflet'; import { bool, choice, json, num, positional, str, type PropDef } from './props.ts'; // Leaflet classes share no common interface, so the prop fragments below // describe structurally what they need from the object they update. export interface Positioned { getLatLng(): LatLng | undefined; setLatLng(latlng: [number, number]): unknown; } export interface Styleable { setStyle(style: PathOptions): unknown; } export interface Sourced { setUrl(url: string): unknown; } export interface Bounded { getBounds(): LatLngBounds; } // Shared `get` for any `bounds` prop backed by Leaflet's getBounds(). Returns // a plain [[south, west], [north, east]] pair rather than the LatLngBounds // instance, matching the JSON-encoded shape the attribute round-trips through. export function getBounds(obj: Bounded): LatLngBoundsExpression { const b = obj.getBounds(); return [ [b.getSouth(), b.getWest()], [b.getNorth(), b.getEast()], ]; } interface PositionedHost extends HTMLElement { lat: number; lng: number; } // A `set` for any option Leaflet only exposes through setStyle(). export function style(key: keyof PathOptions): (obj: Styleable, value: T) => void { return (obj, value) => { obj.setStyle({ [key]: value } as PathOptions); }; } // The source url. Passed positionally by every Leaflet constructor that takes // one, and ignored when blank so clearing the attribute can't request nothing. // No `get`: none of TileLayer/ImageOverlay/VideoOverlay expose a getUrl(). export const urlProp = positional( str('', { set(obj, value) { if (value) obj.setUrl(value); }, }), ); // lat/lng travel together: both are passed positionally to the Leaflet // constructor rather than as options, setting either re-issues setLatLng with // the other's current value, and both are written back whenever the object // moves -- which is what keeps the attributes current while a marker is // dragged. Shared by marker, circle, circle-marker, popup and tooltip. export const latLngProps = { lat: positional( num(0, { event: 'move', get: (obj) => obj.getLatLng()?.lat, set(obj, value, el) { obj.setLatLng([value, (el as PositionedHost).lng]); }, }), ), lng: positional( num(0, { event: 'move', get: (obj) => obj.getLatLng()?.lng, set(obj, value, el) { obj.setLatLng([(el as PositionedHost).lat, value]); }, }), ), } as const; // The style options every Path accepts. Defaults match Leaflet's own, so an // absent attribute and an unset option mean the same thing. export const pathProps = { stroke: bool(true, { set: style('stroke') }), color: str('#3388ff', { set: style('color') }), weight: num(3, { set: style('weight') }), opacity: num(1.0, { set: style('opacity') }), lineCap: choice('round', { set: style('lineCap') }), lineJoin: choice('round', { set: style('lineJoin') }), dashArray: str('', { set: style('dashArray') }), dashOffset: str('', { set: style('dashOffset') }), fill: bool(true, { set: style('fill') }), fillColor: str('#3388ff', { set: style('fillColor') }), fillOpacity: num(0.2, { set: style('fillOpacity') }), fillRule: choice('evenodd', { set: style('fillRule') }), // Constructor-only: Leaflet has no setter for these, so changing the // attribute after creation has no effect (same as leaflet-map's zoomSnap). className: str(), interactive: bool(true), bubblingMouseEvents: bool(true), pane: str('overlay'), } as const; // The GridLayer/TileLayer options every tile source accepts, shared by // leaflet-tile-layer and leaflet-tile-layer-wms (WMSOptions extends // TileLayerOptions). Most are constructor-only -- Leaflet exposes no setter // for them -- so changing the attribute after creation has no effect, same as // leaflet-map's zoomSnap. export const tileLayerProps = { attribution: str(), minZoom: num(0), maxZoom: num(18), opacity: num(1.0), zIndex: num(1), subdomains: str('abc'), tms: bool(), zoomOffset: num(0), zoomReverse: bool(), detectRetina: bool(), crossOrigin: choice(''), // Leaflet's ReferrerPolicy type has no "unset" member (unlike the DOM's), // so this is a plain PropDef rather than choice(), with `undefined` as the // fallback the property getter reports when the attribute is absent. referrerPolicy: { default: undefined, decode: (raw) => raw as ReferrerPolicy, encode: (value) => value ?? null, } as PropDef, errorTileUrl: str(), tileSize: num(256), noWrap: bool(), bounds: json([]), className: str(), minNativeZoom: num(), maxNativeZoom: num(), keepBuffer: num(2), updateWhenIdle: bool(), updateWhenZooming: bool(true), updateInterval: num(200), pane: str('tilePane'), } as const;