Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v8] react-maplibre module #2466

Merged
merged 1 commit into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading