Skip to content

Commit

Permalink
[v8] react-maplibre module (#2466)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Jan 16, 2025
1 parent 69d878a commit 3032b69
Show file tree
Hide file tree
Showing 49 changed files with 3,392 additions and 12 deletions.
2 changes: 1 addition & 1 deletion modules/react-maplibre/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@maplibre/maplibre-gl-style-spec": "^19.2.1"
},
"devDependencies": {
"maplibre-gl": "5.0.0"
"maplibre-gl": "^5.0.0"
},
"peerDependencies": {
"maplibre-gl": ">=4.0.0",
Expand Down
27 changes: 27 additions & 0 deletions modules/react-maplibre/src/components/attribution-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import {useEffect, memo} from 'react';
import {applyReactStyle} from '../utils/apply-react-style';
import {useControl} from './use-control';

import type {ControlPosition, AttributionControlOptions} from '../types/lib';

export type AttributionControlProps = AttributionControlOptions & {
/** Placement of the control relative to the map. */
position?: ControlPosition;
/** CSS style override, applied to the control's container */
style?: React.CSSProperties;
};

function _AttributionControl(props: AttributionControlProps) {
const ctrl = useControl(({mapLib}) => new mapLib.AttributionControl(props), {
position: props.position
});

useEffect(() => {
applyReactStyle(ctrl._container, props.style);
}, [props.style]);

return null;
}

export const AttributionControl = memo(_AttributionControl);
35 changes: 35 additions & 0 deletions modules/react-maplibre/src/components/fullscreen-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* global document */
import * as React from 'react';
import {useEffect, memo} from 'react';
import {applyReactStyle} from '../utils/apply-react-style';
import {useControl} from './use-control';

import type {ControlPosition, FullscreenControlOptions} from '../types/lib';

export type FullscreenControlProps = Omit<FullscreenControlOptions, 'container'> & {
/** Id of the DOM element which should be made full screen. By default, the map container
* element will be made full screen. */
containerId?: string;
/** Placement of the control relative to the map. */
position?: ControlPosition;
/** CSS style override, applied to the control's container */
style?: React.CSSProperties;
};

function _FullscreenControl(props: FullscreenControlProps) {
const ctrl = useControl(
({mapLib}) =>
new mapLib.FullscreenControl({
container: props.containerId && document.getElementById(props.containerId)
}),
{position: props.position}
);

useEffect(() => {
applyReactStyle(ctrl._controlContainer, props.style);
}, [props.style]);

return null;
}

export const FullscreenControl = memo(_FullscreenControl);
81 changes: 81 additions & 0 deletions modules/react-maplibre/src/components/geolocate-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import {useImperativeHandle, useRef, useEffect, forwardRef, memo} from 'react';
import {applyReactStyle} from '../utils/apply-react-style';
import {useControl} from './use-control';

import type {
ControlPosition,
GeolocateControlInstance,
GeolocateControlOptions
} from '../types/lib';
import type {GeolocateEvent, GeolocateResultEvent, GeolocateErrorEvent} from '../types/events';

export type GeolocateControlProps = GeolocateControlOptions & {
/** Placement of the control relative to the map. */
position?: ControlPosition;
/** CSS style override, applied to the control's container */
style?: React.CSSProperties;

/** Called on each Geolocation API position update that returned as success. */
onGeolocate?: (e: GeolocateResultEvent) => void;
/** Called on each Geolocation API position update that returned as an error. */
onError?: (e: GeolocateErrorEvent) => void;
/** Called on each Geolocation API position update that returned as success but user position
* is out of map `maxBounds`. */
onOutOfMaxBounds?: (e: GeolocateResultEvent) => void;
/** Called when the GeolocateControl changes to the active lock state. */
onTrackUserLocationStart?: (e: GeolocateEvent) => void;
/** Called when the GeolocateControl changes to the background state. */
onTrackUserLocationEnd?: (e: GeolocateEvent) => void;
};

function _GeolocateControl(props: GeolocateControlProps, ref: React.Ref<GeolocateControlInstance>) {
const thisRef = useRef({props});

const ctrl = useControl(
({mapLib}) => {
const gc = new mapLib.GeolocateControl(props);

// Hack: fix GeolocateControl reuse
// When using React strict mode, the component is mounted twice.
// GeolocateControl's UI creation is asynchronous. Removing and adding it back causes the UI to be initialized twice.
const setupUI = gc._setupUI;
gc._setupUI = () => {
if (!gc._container.hasChildNodes()) {
setupUI();
}
};

gc.on('geolocate', e => {
thisRef.current.props.onGeolocate?.(e as GeolocateResultEvent);
});
gc.on('error', e => {
thisRef.current.props.onError?.(e as GeolocateErrorEvent);
});
gc.on('outofmaxbounds', e => {
thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateResultEvent);
});
gc.on('trackuserlocationstart', e => {
thisRef.current.props.onTrackUserLocationStart?.(e as GeolocateEvent);
});
gc.on('trackuserlocationend', e => {
thisRef.current.props.onTrackUserLocationEnd?.(e as GeolocateEvent);
});

return gc;
},
{position: props.position}
);

thisRef.current.props = props;

useImperativeHandle(ref, () => ctrl, []);

useEffect(() => {
applyReactStyle(ctrl._container, props.style);
}, [props.style]);

return null;
}

export const GeolocateControl = memo(forwardRef(_GeolocateControl));
125 changes: 125 additions & 0 deletions modules/react-maplibre/src/components/layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {useContext, useEffect, useMemo, useState, useRef} from 'react';
import {MapContext} from './map';
import assert from '../utils/assert';
import {deepEqual} from '../utils/deep-equal';

import type {MapInstance, CustomLayerInterface} from '../types/lib';
import type {AnyLayer} from '../types/style-spec';

// Omiting property from a union type, see
// https://github.com/microsoft/TypeScript/issues/39556#issuecomment-656925230
type OptionalId<T> = T extends {id: string} ? Omit<T, 'id'> & {id?: string} : T;
type OptionalSource<T> = T extends {source: string} ? Omit<T, 'source'> & {source?: string} : T;

export type LayerProps = (OptionalSource<OptionalId<AnyLayer>> | CustomLayerInterface) & {
/** If set, the layer will be inserted before the specified layer */
beforeId?: string;
};

/* eslint-disable complexity, max-statements */
function updateLayer(map: MapInstance, id: string, props: LayerProps, prevProps: LayerProps) {
assert(props.id === prevProps.id, 'layer id changed');
assert(props.type === prevProps.type, 'layer type changed');

if (props.type === 'custom' || prevProps.type === 'custom') {
return;
}

// @ts-ignore filter does not exist in some Layer types
const {layout = {}, paint = {}, filter, minzoom, maxzoom, beforeId} = props;

if (beforeId !== prevProps.beforeId) {
map.moveLayer(id, beforeId);
}
if (layout !== prevProps.layout) {
const prevLayout = prevProps.layout || {};
for (const key in layout) {
if (!deepEqual(layout[key], prevLayout[key])) {
map.setLayoutProperty(id, key, layout[key]);
}
}
for (const key in prevLayout) {
if (!layout.hasOwnProperty(key)) {
map.setLayoutProperty(id, key, undefined);
}
}
}
if (paint !== prevProps.paint) {
const prevPaint = prevProps.paint || {};
for (const key in paint) {
if (!deepEqual(paint[key], prevPaint[key])) {
map.setPaintProperty(id, key, paint[key]);
}
}
for (const key in prevPaint) {
if (!paint.hasOwnProperty(key)) {
map.setPaintProperty(id, key, undefined);
}
}
}

// @ts-ignore filter does not exist in some Layer types
if (!deepEqual(filter, prevProps.filter)) {
map.setFilter(id, filter);
}
if (minzoom !== prevProps.minzoom || maxzoom !== prevProps.maxzoom) {
map.setLayerZoomRange(id, minzoom, maxzoom);
}
}

function createLayer(map: MapInstance, id: string, props: LayerProps) {
// @ts-ignore
if (map.style && map.style._loaded && (!('source' in props) || map.getSource(props.source))) {
const options: LayerProps = {...props, id};
delete options.beforeId;

// @ts-ignore
map.addLayer(options, props.beforeId);
}
}

/* eslint-enable complexity, max-statements */

let layerCounter = 0;

export function Layer(props: LayerProps) {
const map = useContext(MapContext).map.getMap();
const propsRef = useRef(props);
const [, setStyleLoaded] = useState(0);

const id = useMemo(() => props.id || `jsx-layer-${layerCounter++}`, []);

useEffect(() => {
if (map) {
const forceUpdate = () => setStyleLoaded(version => version + 1);
map.on('styledata', forceUpdate);
forceUpdate();

return () => {
map.off('styledata', forceUpdate);
// @ts-ignore
if (map.style && map.style._loaded && map.getLayer(id)) {
map.removeLayer(id);
}
};
}
return undefined;
}, [map]);

// @ts-ignore
const layer = map && map.style && map.getLayer(id);
if (layer) {
try {
updateLayer(map, id, props, propsRef.current);
} catch (error) {
console.warn(error); // eslint-disable-line
}
} else {
createLayer(map, id, props);
}

// Store last rendered props
propsRef.current = props;

return null;
}
25 changes: 25 additions & 0 deletions modules/react-maplibre/src/components/logo-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import {useEffect, memo} from 'react';
import {applyReactStyle} from '../utils/apply-react-style';
import {useControl} from './use-control';

import type {ControlPosition, LogoControlOptions} from '../types/lib';

export type LogoControlProps = LogoControlOptions & {
/** Placement of the control relative to the map. */
position?: ControlPosition;
/** CSS style override, applied to the control's container */
style?: React.CSSProperties;
};

function _LogoControl(props: LogoControlProps) {
const ctrl = useControl(({mapLib}) => new mapLib.LogoControl(props), {position: props.position});

useEffect(() => {
applyReactStyle(ctrl._container, props.style);
}, [props.style]);

return null;
}

export const LogoControl = memo(_LogoControl);
Loading

0 comments on commit 3032b69

Please sign in to comment.