2023-08-31 18:16:14 +02:00
|
|
|
import {
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useState,
|
|
|
|
useMemo,
|
|
|
|
type RefObject
|
|
|
|
} from 'react';
|
2022-02-08 12:49:51 +01:00
|
|
|
import type {
|
|
|
|
LngLatBoundsLike,
|
|
|
|
LngLat,
|
|
|
|
MapLayerEventType,
|
2024-04-26 19:54:37 +02:00
|
|
|
Style,
|
|
|
|
LngLatLike
|
2022-02-08 12:49:51 +01:00
|
|
|
} from 'maplibre-gl';
|
|
|
|
import type { Feature, Geometry } from 'geojson';
|
|
|
|
|
|
|
|
import { getMapStyle, getLayerName, LayersMap } from './styles';
|
|
|
|
import { useMapLibre } from './MapLibre';
|
|
|
|
|
|
|
|
export function useFitBounds() {
|
|
|
|
const map = useMapLibre();
|
2022-02-22 13:14:11 +01:00
|
|
|
return useCallback(
|
|
|
|
(bbox: LngLatBoundsLike) => {
|
|
|
|
map.fitBounds(bbox, { padding: 100 });
|
|
|
|
},
|
|
|
|
[map]
|
|
|
|
);
|
2022-02-08 12:49:51 +01:00
|
|
|
}
|
|
|
|
|
2024-02-01 17:34:02 +01:00
|
|
|
export function useFitBoundsNoFly() {
|
|
|
|
const map = useMapLibre();
|
|
|
|
return useCallback(
|
|
|
|
(bbox: LngLatBoundsLike) => {
|
|
|
|
map.fitBounds(bbox, { padding: 100, linear: true, duration: 0 });
|
|
|
|
},
|
|
|
|
[map]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-02-08 12:49:51 +01:00
|
|
|
export function useFlyTo() {
|
|
|
|
const map = useMapLibre();
|
2022-02-22 13:14:11 +01:00
|
|
|
return useCallback(
|
2024-04-26 19:54:37 +02:00
|
|
|
(zoom: number, center: LngLatLike) => {
|
2022-02-22 13:14:11 +01:00
|
|
|
map.flyTo({ zoom, center });
|
|
|
|
},
|
|
|
|
[map]
|
|
|
|
);
|
2022-02-08 12:49:51 +01:00
|
|
|
}
|
|
|
|
|
2024-04-26 19:54:37 +02:00
|
|
|
export function useEvent<T>(
|
|
|
|
eventName: string,
|
|
|
|
callback: (event: CustomEvent<T>) => void
|
|
|
|
) {
|
2022-02-08 12:49:51 +01:00
|
|
|
return useEffect(() => {
|
2024-04-26 19:54:37 +02:00
|
|
|
addEventListener(eventName, callback as EventListener);
|
|
|
|
return () => removeEventListener(eventName, callback as EventListener);
|
2022-02-08 12:49:51 +01:00
|
|
|
}, [eventName, callback]);
|
|
|
|
}
|
|
|
|
|
|
|
|
export type EventHandler = (event: {
|
|
|
|
features: Feature<Geometry>[];
|
|
|
|
lngLat: LngLat;
|
|
|
|
}) => void;
|
|
|
|
|
|
|
|
export function useMapEvent(
|
|
|
|
eventName: string,
|
|
|
|
callback: EventHandler,
|
|
|
|
target?: string
|
|
|
|
) {
|
|
|
|
const map = useMapLibre();
|
|
|
|
return useEffect(() => {
|
|
|
|
if (target) {
|
2022-02-23 13:10:50 +01:00
|
|
|
// event typing is hard
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-02-08 12:49:51 +01:00
|
|
|
map.on(eventName as keyof MapLayerEventType, target, callback as any);
|
|
|
|
} else {
|
|
|
|
map.on(eventName, callback);
|
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
if (target) {
|
2022-02-23 13:10:50 +01:00
|
|
|
// event typing is hard
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-02-08 12:49:51 +01:00
|
|
|
map.off(eventName as keyof MapLayerEventType, target, callback as any);
|
|
|
|
} else {
|
|
|
|
map.off(eventName, callback);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [map, eventName, target, callback]);
|
|
|
|
}
|
|
|
|
|
|
|
|
function optionalLayersMap(optionalLayers: string[]): LayersMap {
|
|
|
|
return Object.fromEntries(
|
|
|
|
optionalLayers.map((layer) => [
|
|
|
|
layer,
|
|
|
|
{
|
|
|
|
configurable: layer != 'cadastres',
|
|
|
|
enabled: true,
|
|
|
|
opacity: 70,
|
|
|
|
name: getLayerName(layer)
|
|
|
|
}
|
|
|
|
])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useStyle(
|
|
|
|
optionalLayers: string[],
|
|
|
|
onStyleChange: (style: Style) => void
|
|
|
|
) {
|
|
|
|
const [styleId, setStyle] = useState('ortho');
|
|
|
|
const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));
|
|
|
|
const setLayerEnabled = (layer: string, enabled: boolean) =>
|
|
|
|
setLayers((optionalLayers) => {
|
|
|
|
optionalLayers[layer].enabled = enabled;
|
|
|
|
return { ...optionalLayers };
|
|
|
|
});
|
|
|
|
const setLayerOpacity = (layer: string, opacity: number) =>
|
|
|
|
setLayers((optionalLayers) => {
|
|
|
|
optionalLayers[layer].opacity = opacity;
|
|
|
|
return { ...optionalLayers };
|
|
|
|
});
|
|
|
|
const enabledLayers = useMemo(
|
|
|
|
() => Object.entries(layers).filter(([, { enabled }]) => enabled),
|
|
|
|
[layers]
|
|
|
|
);
|
|
|
|
const style = useMemo(
|
|
|
|
() =>
|
|
|
|
getMapStyle(
|
|
|
|
styleId,
|
|
|
|
enabledLayers.map(([layer]) => layer),
|
|
|
|
Object.fromEntries(
|
|
|
|
enabledLayers.map(([layer, { opacity }]) => [layer, opacity])
|
|
|
|
)
|
|
|
|
),
|
|
|
|
[styleId, enabledLayers]
|
|
|
|
);
|
|
|
|
|
2022-02-22 13:14:11 +01:00
|
|
|
useEffect(() => onStyleChange(style), [onStyleChange, style]);
|
2022-02-08 12:49:51 +01:00
|
|
|
|
|
|
|
return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
|
|
|
|
}
|
2023-08-31 18:16:14 +02:00
|
|
|
|
|
|
|
function isElementVisible(
|
|
|
|
element: HTMLElement,
|
|
|
|
callback: (visible: boolean) => void
|
|
|
|
) {
|
|
|
|
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
|
|
|
callback(true);
|
|
|
|
} else {
|
|
|
|
callback(false);
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
(entries) => callback(entries[0].isIntersecting == true),
|
|
|
|
{ threshold: [0] }
|
|
|
|
);
|
|
|
|
observer.observe(element);
|
|
|
|
return () => observer.unobserve(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useElementVisible(element: RefObject<HTMLElement>) {
|
|
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
|
|
if (element.current) {
|
|
|
|
return isElementVisible(element.current, setVisible);
|
|
|
|
}
|
|
|
|
}, [element]);
|
|
|
|
return visible;
|
|
|
|
}
|