@octanejs/react-map-gl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'octane';
2
+ import { MapContext } from './map.tsrx';
3
+ import assert from '../utils/assert';
4
+ import { deepEqual } from '../utils/deep-equal';
5
+
6
+ import type {
7
+ SourceSpecification,
8
+ CanvasSourceSpecification,
9
+ ImageSourceSpecification,
10
+ VectorSourceSpecification,
11
+ } from '../types/style-spec';
12
+ import type { MapInstance } from '../types/lib';
13
+ import type {
14
+ GeoJSONSourceImplementation,
15
+ ImageSourceImplementation,
16
+ AnySourceImplementation,
17
+ } from '../types/internal';
18
+ import type { MapChildren } from '../types/octane';
19
+
20
+ export type SourceProps =
21
+ (SourceSpecification | CanvasSourceSpecification) & {
22
+ id?: string;
23
+ children?: MapChildren;
24
+ };
25
+
26
+ /**
27
+ * OCTANE DIVERGENCE[react-map-gl-source-id-by-context][ported:source-layer:3]: upstream delivers this source's id to child layers with
28
+ * `Children.map(children, child => cloneElement(child, {source: id}))`. Octane
29
+ * lowers a `.tsrx` parent's children to a render function, which cannot be
30
+ * cloned, so the id travels by context instead and `Layer` reads it.
31
+ *
32
+ * Two observable consequences, both pinned by tests:
33
+ * - the id reaches any descendant `Layer`, not only a direct child;
34
+ * - as with upstream's cloneElement, the enclosing source still overrides a
35
+ * `source` prop set explicitly on the layer.
36
+ */
37
+ export const SourceContext = createContext<string | null>(null);
38
+
39
+ let sourceCounter = 0;
40
+
41
+ function createSource(map: MapInstance, id: string, props: SourceProps) {
42
+ // @ts-ignore
43
+ if (map.style && map.style._loaded) {
44
+ const options = { ...props };
45
+ delete options.id;
46
+ delete options.children;
47
+ map.addSource(id, options as unknown as SourceSpecification);
48
+ return map.getSource(id);
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /* eslint-disable complexity */
54
+ function updateSource(source: AnySourceImplementation, props: SourceProps, prevProps: SourceProps) {
55
+ assert(props.id === prevProps.id, 'source id changed');
56
+ assert(props.type === prevProps.type, 'source type changed');
57
+
58
+ let changedKey = '';
59
+ let changedKeyCount = 0;
60
+
61
+ const nextValues = props as unknown as Record<string, unknown>;
62
+ const prevValues = prevProps as unknown as Record<string, unknown>;
63
+ for (const key in props) {
64
+ if (key !== 'children' && key !== 'id' && !deepEqual(prevValues[key], nextValues[key])) {
65
+ changedKey = key;
66
+ changedKeyCount++;
67
+ }
68
+ }
69
+
70
+ if (!changedKeyCount) {
71
+ return;
72
+ }
73
+
74
+ const type = props.type;
75
+
76
+ if (type === 'geojson') {
77
+ (source as GeoJSONSourceImplementation).setData(props.data!);
78
+ } else if (type === 'image') {
79
+ (source as ImageSourceImplementation).updateImage({
80
+ url: props.url!,
81
+ coordinates: props.coordinates,
82
+ });
83
+ } else if ('setCoordinates' in source && changedKeyCount === 1 && changedKey === 'coordinates') {
84
+ source.setCoordinates((props as unknown as ImageSourceSpecification).coordinates);
85
+ } else if ('setUrl' in source && changedKey === 'url') {
86
+ source.setUrl((props as VectorSourceSpecification).url!);
87
+ } else if ('setTiles' in source && changedKey === 'tiles') {
88
+ source.setTiles((props as VectorSourceSpecification).tiles!);
89
+ } else {
90
+ // eslint-disable-next-line
91
+ console.warn(`Unable to update <Source> prop: ${changedKey}`);
92
+ }
93
+ }
94
+ /* eslint-enable complexity */
95
+
96
+ export function Source(props: SourceProps) @{
97
+ const map = useContext(MapContext).map.getMap();
98
+ const propsRef = useRef(props);
99
+ const [, setStyleLoaded] = useState(0);
100
+
101
+ const id = useMemo(() => props.id || `jsx-source-${sourceCounter++}`, []);
102
+
103
+ useEffect(() => {
104
+ if (map) {
105
+ /* global setTimeout */
106
+ const forceUpdate = () => setTimeout(() => setStyleLoaded((version) => version + 1), 0);
107
+ map.on('styledata', forceUpdate);
108
+ forceUpdate();
109
+
110
+ return () => {
111
+ map.off('styledata', forceUpdate);
112
+ // @ts-ignore
113
+ if (map.style && map.style._loaded && map.getSource(id)) {
114
+ // Parent effects are destroyed before child ones, see
115
+ // https://github.com/facebook/react/issues/16728
116
+ // Source can only be removed after all child layers are removed
117
+ const allLayers = map.getStyle()?.layers;
118
+ if (allLayers) {
119
+ for (const layer of allLayers) {
120
+ // @ts-ignore (2339) source does not exist on all layer types
121
+ if (layer.source === id) {
122
+ map.removeLayer(layer.id);
123
+ }
124
+ }
125
+ }
126
+ map.removeSource(id);
127
+ }
128
+ };
129
+ }
130
+ return undefined;
131
+ }, [map]);
132
+
133
+ let source = (map && map.style && map.getSource(id)) as AnySourceImplementation | null;
134
+ if (source) {
135
+ updateSource(source, props, propsRef.current);
136
+ } else {
137
+ source = createSource(map, id, props) as AnySourceImplementation | null;
138
+ }
139
+ propsRef.current = props;
140
+
141
+ <>
142
+ @if (source) {
143
+ <SourceContext.Provider value={id}>{props.children}</SourceContext.Provider>
144
+ }
145
+ </>
146
+ }
@@ -0,0 +1,73 @@
1
+ import { useContext, useEffect, useMemo } from 'octane';
2
+ import type { ControlPosition, IControl } from '../types/lib';
3
+ import { MapContext } from './map.tsrx';
4
+ import type { MapContextValue } from './map.tsrx';
5
+
6
+ type ControlOptions = {
7
+ position?: ControlPosition;
8
+ };
9
+
10
+ export function useControl<T extends IControl>(
11
+ onCreate: (context: MapContextValue) => T,
12
+ opts?: ControlOptions,
13
+ ): T;
14
+
15
+ export function useControl<T extends IControl>(
16
+ onCreate: (context: MapContextValue) => T,
17
+ onRemove: (context: MapContextValue) => void,
18
+ opts?: ControlOptions,
19
+ ): T;
20
+
21
+ export function useControl<T extends IControl>(
22
+ onCreate: (context: MapContextValue) => T,
23
+ onAdd: (context: MapContextValue) => void,
24
+ onRemove: (context: MapContextValue) => void,
25
+ opts?: ControlOptions,
26
+ ): T;
27
+
28
+ export function useControl<T extends IControl>(
29
+ onCreate: (context: MapContextValue) => T,
30
+ arg1?: ((context: MapContextValue) => void) | ControlOptions,
31
+ arg2?: ((context: MapContextValue) => void) | ControlOptions,
32
+ arg3?: ControlOptions,
33
+ ): T {
34
+ // Octane's compiler rewrites a custom-hook call site in a `.tsrx` module to
35
+ // `withSlot(sym, useControl, ...args, sym)` and deliberately retains that
36
+ // trailing symbol, so bindings that read their slot off the last argument
37
+ // keep working. This hook resolves its optional trailing parameters
38
+ // positionally, so an un-stripped symbol wins the `arg3 || arg2 || arg1`
39
+ // race and the caller's options disappear — a control silently lands in the
40
+ // default corner. No legitimate argument here is ever a symbol.
41
+ if (typeof arg1 === 'symbol') arg1 = undefined;
42
+ if (typeof arg2 === 'symbol') arg2 = undefined;
43
+ if (typeof arg3 === 'symbol') arg3 = undefined;
44
+
45
+ const context = useContext(MapContext);
46
+ const ctrl = useMemo(() => onCreate(context), []);
47
+
48
+ useEffect(() => {
49
+ const opts = (arg3 || arg2 || arg1) as ControlOptions;
50
+ const onAdd = typeof arg1 === 'function' && typeof arg2 === 'function' ? arg1 : null;
51
+ const onRemove = typeof arg2 === 'function' ? arg2 : typeof arg1 === 'function' ? arg1 : null;
52
+
53
+ const { map } = context;
54
+ if (!map.hasControl(ctrl)) {
55
+ map.addControl(ctrl, opts?.position);
56
+ if (onAdd) {
57
+ onAdd(context);
58
+ }
59
+ }
60
+
61
+ return () => {
62
+ if (onRemove) {
63
+ onRemove(context);
64
+ }
65
+ // Map might have been removed (parent effects are destroyed before child ones)
66
+ if (map.hasControl(ctrl)) {
67
+ map.removeControl(ctrl);
68
+ }
69
+ };
70
+ }, []);
71
+
72
+ return ctrl;
73
+ }
@@ -0,0 +1,64 @@
1
+ import { createContext, useCallback, useContext, useMemo, useState } from 'octane';
2
+
3
+ import type { MapRef } from '../mapbox/create-ref';
4
+ import { MapContext } from './map.tsrx';
5
+ import type { MapChildren } from '../types/octane';
6
+
7
+ type MountedMapsContextValue = {
8
+ maps: { [id: string]: MapRef };
9
+ onMapMount: (map: MapRef, id?: string) => void;
10
+ onMapUnmount: (id?: string) => void;
11
+ };
12
+
13
+ export const MountedMapsContext = createContext<MountedMapsContextValue | null>(null);
14
+
15
+ export function MapProvider(props: { children?: MapChildren }) @{
16
+ const [maps, setMaps] = useState<{ [id: string]: MapRef }>({});
17
+
18
+ const onMapMount = useCallback((map: MapRef, id: string = 'default') => {
19
+ setMaps((currMaps) => {
20
+ if (id === 'current') {
21
+ throw new Error('\'current\' cannot be used as map id');
22
+ }
23
+ if (currMaps[id]) {
24
+ throw new Error(`Multiple maps with the same id: ${id}`);
25
+ }
26
+ return { ...currMaps, [id]: map };
27
+ });
28
+ }, []);
29
+
30
+ const onMapUnmount = useCallback((id: string = 'default') => {
31
+ setMaps((currMaps) => {
32
+ if (currMaps[id]) {
33
+ const nextMaps = { ...currMaps };
34
+ delete nextMaps[id];
35
+ return nextMaps;
36
+ }
37
+ return currMaps;
38
+ });
39
+ }, []);
40
+
41
+ <MountedMapsContext.Provider
42
+ value={{
43
+ maps,
44
+ onMapMount,
45
+ onMapUnmount,
46
+ }}
47
+ >{props.children}</MountedMapsContext.Provider>
48
+ }
49
+
50
+ export type MapCollection = {
51
+ [id: string]: MapRef | undefined;
52
+ current?: MapRef;
53
+ };
54
+
55
+ export function useMap(): MapCollection {
56
+ const maps = useContext(MountedMapsContext)?.maps;
57
+ const currentMap = useContext(MapContext);
58
+
59
+ const mapsWithCurrent = useMemo(() => {
60
+ return { ...maps, current: currentMap?.map };
61
+ }, [maps, currentMap]);
62
+
63
+ return mapsWithCurrent as MapCollection;
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Map } from './components/map.tsrx';
2
+ export { Map };
3
+ export default Map;
4
+
5
+ export { Marker } from './components/marker';
6
+ export { Popup } from './components/popup';
7
+ export { AttributionControl } from './components/attribution-control';
8
+ export { FullscreenControl } from './components/fullscreen-control';
9
+ export { GeolocateControl } from './components/geolocate-control';
10
+ export { NavigationControl } from './components/navigation-control';
11
+ export { ScaleControl } from './components/scale-control';
12
+ export { Source } from './components/source.tsrx';
13
+ export { Layer } from './components/layer';
14
+ export { useControl } from './components/use-control';
15
+ export { MapProvider, useMap } from './components/use-map.tsrx';
16
+
17
+ export type { MapProps } from './components/map.tsrx';
18
+ export type { MapRef } from './mapbox/create-ref';
19
+ export type { MarkerProps } from './components/marker';
20
+ export type { PopupProps } from './components/popup';
21
+ export type { AttributionControlProps } from './components/attribution-control';
22
+ export type { FullscreenControlProps } from './components/fullscreen-control';
23
+ export type { GeolocateControlProps } from './components/geolocate-control';
24
+ export type { NavigationControlProps } from './components/navigation-control';
25
+ export type { ScaleControlProps } from './components/scale-control';
26
+ export type { SourceProps } from './components/source.tsrx';
27
+ export type { LayerProps } from './components/layer';
28
+
29
+ // Types
30
+ export * from './types/common';
31
+ export * from './types/events';
32
+ export * from './types/lib';
33
+ export * from './types/style-spec';
@@ -0,0 +1,114 @@
1
+ // @ts-nocheck — byte-exact reuse of @vis.gl/react-mapbox 8.1.2. Upstream compiles
2
+ // without strictNullChecks or noImplicitAny; this repository is strict. The copy
3
+ // under upstream/ is typechecked with upstream's own configuration instead, so
4
+ // nothing here is unchecked, only checked at the settings its authors chose.
5
+ // Every line below this four-line banner is byte-identical to upstream.
6
+ import type {MapInstance} from '../types/lib';
7
+ import {LngLatLike, PointLike} from '../types/common';
8
+
9
+ import type Mapbox from './mapbox';
10
+
11
+ /** These methods may break the react binding if called directly */
12
+ const skipMethods = [
13
+ 'setMaxBounds',
14
+ 'setMinZoom',
15
+ 'setMaxZoom',
16
+ 'setMinPitch',
17
+ 'setMaxPitch',
18
+ 'setRenderWorldCopies',
19
+ 'setProjection',
20
+ 'setStyle',
21
+ 'addSource',
22
+ 'removeSource',
23
+ 'addLayer',
24
+ 'removeLayer',
25
+ 'setLayerZoomRange',
26
+ 'setFilter',
27
+ 'setPaintProperty',
28
+ 'setLayoutProperty',
29
+ 'setLight',
30
+ 'setTerrain',
31
+ 'setFog',
32
+ 'remove'
33
+ ] as const;
34
+
35
+ export type MapRef = {
36
+ getMap(): MapInstance;
37
+ } & Omit<MapInstance, (typeof skipMethods)[number]>;
38
+
39
+ export default function createRef(mapInstance: Mapbox): MapRef | null {
40
+ if (!mapInstance) {
41
+ return null;
42
+ }
43
+
44
+ const map = mapInstance.map;
45
+ const ref: any = {
46
+ getMap: () => map,
47
+
48
+ // Overwrite getters to use our shadow transform
49
+ getCenter: () => mapInstance.transform.center,
50
+ getZoom: () => mapInstance.transform.zoom,
51
+ getBearing: () => mapInstance.transform.bearing,
52
+ getPitch: () => mapInstance.transform.pitch,
53
+ getPadding: () => mapInstance.transform.padding,
54
+ getBounds: () => mapInstance.transform.getBounds(),
55
+ project: (lnglat: LngLatLike) => {
56
+ const tr = map.transform;
57
+ map.transform = mapInstance.transform;
58
+ const result = map.project(lnglat);
59
+ map.transform = tr;
60
+ return result;
61
+ },
62
+ unproject: (point: PointLike) => {
63
+ const tr = map.transform;
64
+ map.transform = mapInstance.transform;
65
+ const result = map.unproject(point);
66
+ map.transform = tr;
67
+ return result;
68
+ },
69
+ // options diverge between mapbox and maplibre
70
+ queryTerrainElevation: (lnglat: LngLatLike, options?: any) => {
71
+ const tr = map.transform;
72
+ map.transform = mapInstance.transform;
73
+ const result = map.queryTerrainElevation(lnglat, options);
74
+ map.transform = tr;
75
+ return result;
76
+ },
77
+ queryRenderedFeatures: (geometry?: any, options?: any) => {
78
+ const tr = map.transform;
79
+ map.transform = mapInstance.transform;
80
+ const result = map.queryRenderedFeatures(geometry, options);
81
+ map.transform = tr;
82
+ return result;
83
+ }
84
+ };
85
+
86
+ for (const key of getMethodNames(map)) {
87
+ // @ts-expect-error
88
+ if (!(key in ref) && !skipMethods.includes(key)) {
89
+ ref[key] = map[key].bind(map);
90
+ }
91
+ }
92
+
93
+ return ref;
94
+ }
95
+
96
+ function getMethodNames(obj: Object) {
97
+ const result = new Set<string>();
98
+
99
+ let proto = obj;
100
+ while (proto) {
101
+ for (const key of Object.getOwnPropertyNames(proto)) {
102
+ if (
103
+ key[0] !== '_' &&
104
+ typeof obj[key] === 'function' &&
105
+ key !== 'fire' &&
106
+ key !== 'setEventedParent'
107
+ ) {
108
+ result.add(key);
109
+ }
110
+ }
111
+ proto = Object.getPrototypeOf(proto);
112
+ }
113
+ return Array.from(result);
114
+ }