facilmap/leaflet/src/overpass/overpass-layer.ts

164 wiersze
4.8 KiB
TypeScript

import { Colour, Shape } from "facilmap-types";
import { FeatureGroup, latLng, Layer, LayerOptions } from "leaflet";
import MarkerLayer from "../markers/marker-layer";
import { getSymbolForTags } from "../utils/icons";
import { tooltipOptions } from "../utils/leaflet";
import { OverpassPreset } from "./overpass-presets";
import { getOverpassElements, isOverpassQueryEmpty, OverpassElement } from "./overpass-utils";
declare module "leaflet" {
interface Layer {
_fmOverpassElement?: OverpassElement;
}
}
function getElementIdentifier(element: OverpassElement): string {
return `${element.type}${element.id}`;
}
export enum OverpassLoadStatus {
COMPLETE = "complete",
INCOMPLETE = "incomplete",
TIMEOUT = "timeout",
ERROR = "error",
ABORTED = "aborted"
};
export interface OverpassLayerOptions extends LayerOptions {
markerColour?: Colour;
markerSize?: number;
markerShape?: Shape;
timeout?: number;
limit?: number;
}
export default class OverpassLayer extends FeatureGroup {
declare options: OverpassLayerOptions;
_highlightedElements = new Set<string>();
_query: string | OverpassPreset[] | undefined;
_lastRequestController?: AbortController;
constructor(query?: string | OverpassPreset[], options?: OverpassLayerOptions) {
super([], {
markerColour: "000000",
markerSize: 35,
markerShape: "",
timeout: 1,
limit: 50,
...options
});
this._query = query;
}
getEvents(): ReturnType<NonNullable<Layer['getEvents']>> {
return {
moveend: this.redraw
};
}
onAdd(): this {
this.redraw();
return this;
}
isEmpty(): boolean {
return isOverpassQueryEmpty(this._query);
}
getQuery(): string | OverpassPreset[] | undefined {
return this._query;
}
setQuery(query?: string | OverpassPreset[]): void {
this._query = query;
this.redraw();
this.fire("setQuery", { query });
if (this.isEmpty())
this.fire("clear");
}
highlightElement(element: OverpassElement): void {
const identifier = getElementIdentifier(element);
for (const layer of this.getLayers()) {
if (identifier == getElementIdentifier(layer._fmOverpassElement!))
(layer as MarkerLayer).setStyle({ highlight: true, raised: true });
}
this._highlightedElements.add(getElementIdentifier(element));
}
unhighlightElement(element: OverpassElement): void {
const identifier = getElementIdentifier(element);
for (const layer of this.getLayers()) {
if (identifier == getElementIdentifier(layer._fmOverpassElement!))
(layer as MarkerLayer).setStyle({ highlight: false, raised: false });
}
this._highlightedElements.delete(getElementIdentifier(element));
}
setHighlightedElements(elements: Set<OverpassElement>): void {
const identifiers = new Set([...elements].map((element) => getElementIdentifier(element)));
for (const layer of this.getLayers() as MarkerLayer[]) {
const shouldHighlight = identifiers.has(getElementIdentifier(layer._fmOverpassElement!));
if (layer.options.highlight != shouldHighlight)
layer.setStyle({ highlight: shouldHighlight, raised: shouldHighlight });
}
this._highlightedElements = new Set([...elements].map((el) => getElementIdentifier(el)));
}
_elementToLayer(element: OverpassElement): MarkerLayer {
const isHighlighted = this._highlightedElements.has(getElementIdentifier(element));
const layer = new MarkerLayer(latLng(element.lat, element.lon), {
marker: {
colour: this.options.markerColour!,
size: this.options.markerSize!,
symbol: getSymbolForTags(element.tags),
shape: this.options.markerShape!
},
raised: isHighlighted,
highlight: isHighlighted
});
if (element.tags.name)
layer.bindTooltip(element.tags.name, { ...tooltipOptions, offset: [ 20, -20 ] })
layer._fmOverpassElement = element;
return layer;
}
async redraw(): Promise<void> {
if (this._lastRequestController)
this._lastRequestController.abort();
if (!this._map?._loaded)
return;
if (this.isEmpty()) {
this.clearLayers();
return;
}
this.fire("loadstart");
try {
this._lastRequestController = new AbortController();
const elements = await getOverpassElements(this._query!, this._map.getBounds(), this.options.timeout!, this.options.limit!, this._lastRequestController.signal);
this.clearLayers();
for (const element of elements)
this.addLayer(this._elementToLayer(element));
this.fire("loadend", { status: elements.length < this.options.limit! ? OverpassLoadStatus.COMPLETE : OverpassLoadStatus.INCOMPLETE });
} catch (error: any) {
if (error.name == "AbortError") {
this.fire("loadend", { status: OverpassLoadStatus.ABORTED });
return;
}
this.clearLayers();
if (error.message.includes("timed out"))
this.fire("loadend", { status: OverpassLoadStatus.TIMEOUT, error });
else
this.fire("loadend", { status: OverpassLoadStatus.ERROR, error });
}
}
}