@performant-software/geospatial 1.1.3-beta.4 → 1.1.3-beta.6
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.
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/main.css +37 -3
- package/package.json +3 -2
- package/src/components/GeoJsonLayer.js +118 -0
- package/src/components/LayerMenu.css +38 -0
- package/src/components/LayerMenu.js +158 -0
- package/src/components/LayerMenuContainer.js +80 -0
- package/src/components/MapDraw.css +1 -4
- package/src/components/MapDraw.js +40 -24
- package/src/components/RasterLayer.js +36 -0
- package/src/index.js +3 -0
- package/types/components/GeoJsonLayer.js.flow +118 -0
- package/types/components/LayerMenu.js.flow +158 -0
- package/types/components/LayerMenuContainer.js.flow +80 -0
- package/types/components/MapDraw.js.flow +40 -24
- package/types/components/RasterLayer.js.flow +36 -0
- package/types/index.js.flow +3 -0
- package/webpack.config.js +4 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
Children,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo, useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { BsStack } from 'react-icons/bs';
|
|
11
|
+
import { IoCheckmarkOutline } from 'react-icons/io5';
|
|
12
|
+
import _ from 'underscore';
|
|
13
|
+
import LayerMenuContainer from './LayerMenuContainer';
|
|
14
|
+
import './LayerMenu.css';
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
children: Node,
|
|
18
|
+
names: Array<string>,
|
|
19
|
+
position: 'top-left' | 'bottom-left' | 'top-right' | 'bottom-right'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MENU_PADDING = 30;
|
|
23
|
+
|
|
24
|
+
const LayerMenu = (props: Props) => {
|
|
25
|
+
const [canvasHeight, setCanvasHeight] = useState(0);
|
|
26
|
+
const [visible, setVisible] = useState();
|
|
27
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
28
|
+
|
|
29
|
+
const mapRef = useRef();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the name of the layer at the passed index.
|
|
33
|
+
*
|
|
34
|
+
* @type {unknown}
|
|
35
|
+
*/
|
|
36
|
+
const getLayerName = useCallback((index) => (
|
|
37
|
+
props.names && props.names.length > index && props.names[index]
|
|
38
|
+
), [props.names]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the child element at the passed index is visible.
|
|
42
|
+
*
|
|
43
|
+
* @type {function(*): *}
|
|
44
|
+
*/
|
|
45
|
+
const isVisible = useCallback((index) => _.includes(visible, index), [visible]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a memoized array of the child elements.
|
|
49
|
+
*
|
|
50
|
+
* @type {Array<$NonMaybeType<unknown>>}
|
|
51
|
+
*/
|
|
52
|
+
const children = useMemo(() => Children.toArray(props.children), [props.children]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a memoized array of visible child elements.
|
|
56
|
+
*/
|
|
57
|
+
const visibleChildren = useMemo(() => _.filter(children, (child, index) => isVisible(index)), [children, isVisible]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Toggles the visibility for the child element at the passed index.
|
|
61
|
+
*
|
|
62
|
+
* @type {(function(*): void)|*}
|
|
63
|
+
*/
|
|
64
|
+
const toggleVisibility = useCallback((index) => {
|
|
65
|
+
let value;
|
|
66
|
+
|
|
67
|
+
if (isVisible(index)) {
|
|
68
|
+
value = _.without(visible, index);
|
|
69
|
+
} else {
|
|
70
|
+
value = [...visible, index];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setVisible(value);
|
|
74
|
+
}, [isVisible, visible]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sets all of the child elements to be visible when the component mounts.
|
|
78
|
+
*/
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
setVisible(_.times(children.length, (index) => index));
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sets the map canvas height.
|
|
85
|
+
*/
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const { current: instance } = mapRef;
|
|
88
|
+
|
|
89
|
+
if (instance && instance._canvas) {
|
|
90
|
+
const { offsetHeight = 0 } = mapRef.current._canvas;
|
|
91
|
+
setCanvasHeight(offsetHeight);
|
|
92
|
+
}
|
|
93
|
+
}, [mapRef.current]);
|
|
94
|
+
|
|
95
|
+
if (_.isEmpty(children)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<LayerMenuContainer
|
|
102
|
+
mapRef={mapRef}
|
|
103
|
+
position={props.position}
|
|
104
|
+
>
|
|
105
|
+
<button
|
|
106
|
+
className='mapbox-gl-draw_ctrl-draw-btn layer-button'
|
|
107
|
+
onClick={() => setMenuOpen((prevMenuOpen) => !prevMenuOpen)}
|
|
108
|
+
type='button'
|
|
109
|
+
>
|
|
110
|
+
<BsStack
|
|
111
|
+
size='1.25em'
|
|
112
|
+
/>
|
|
113
|
+
</button>
|
|
114
|
+
{ menuOpen && (
|
|
115
|
+
<div
|
|
116
|
+
className='layer-menu'
|
|
117
|
+
style={{
|
|
118
|
+
maxHeight: `calc(${canvasHeight}px - ${MENU_PADDING}px)`
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className='menu'
|
|
123
|
+
>
|
|
124
|
+
{ _.map(children, (child, index) => (
|
|
125
|
+
<div
|
|
126
|
+
aria-selected={isVisible(index)}
|
|
127
|
+
className='option'
|
|
128
|
+
role='option'
|
|
129
|
+
onClick={() => toggleVisibility(index)}
|
|
130
|
+
onKeyDown={() => toggleVisibility(index)}
|
|
131
|
+
tabIndex={index}
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
className='checkmark-container'
|
|
135
|
+
>
|
|
136
|
+
{ isVisible(index) && (
|
|
137
|
+
<IoCheckmarkOutline
|
|
138
|
+
size='1em'
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
{ getLayerName(index) }
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</LayerMenuContainer>
|
|
149
|
+
{ visibleChildren }
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
LayerMenu.defaultProps = {
|
|
155
|
+
position: 'top-left'
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default LayerMenu;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { createPortal } from 'react-dom';
|
|
10
|
+
import { MapboxMap, useControl } from 'react-map-gl';
|
|
11
|
+
import { IControl } from 'maplibre-gl';
|
|
12
|
+
import _ from 'underscore';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Layer control implementation.
|
|
16
|
+
*/
|
|
17
|
+
class LayerControl implements IControl {
|
|
18
|
+
_map: MapboxMap = null;
|
|
19
|
+
_container: HTMLElement;
|
|
20
|
+
_redraw: () => void;
|
|
21
|
+
|
|
22
|
+
constructor(redraw: () => void) {
|
|
23
|
+
this._redraw = redraw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onAdd(map) {
|
|
27
|
+
this._map = map;
|
|
28
|
+
map.on('move', this._redraw);
|
|
29
|
+
|
|
30
|
+
this._container = document.createElement('div');
|
|
31
|
+
this._container.className = 'maplibregl-ctrl-group maplibregl-ctrl';
|
|
32
|
+
this._redraw();
|
|
33
|
+
return this._container;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onRemove() {
|
|
37
|
+
this._container.remove();
|
|
38
|
+
this._map.off('move', this._redraw);
|
|
39
|
+
this._map = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getMap() {
|
|
43
|
+
return this._map;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getElement() {
|
|
47
|
+
return this._container;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Layer container.
|
|
53
|
+
*
|
|
54
|
+
* @param props
|
|
55
|
+
*
|
|
56
|
+
* @returns {React$Portal}
|
|
57
|
+
*/
|
|
58
|
+
const LayerMenuContainer = (props) => {
|
|
59
|
+
const [, setVersion] = useState(0);
|
|
60
|
+
|
|
61
|
+
const ctrl = useControl(() => {
|
|
62
|
+
const forceUpdate = () => setVersion((v) => v + 1);
|
|
63
|
+
return new LayerControl(forceUpdate);
|
|
64
|
+
}, { position: props.position });
|
|
65
|
+
|
|
66
|
+
const map = ctrl.getMap();
|
|
67
|
+
|
|
68
|
+
const children = Children.map(_.compact(props.children), (child) => cloneElement(child, { map }));
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (props.mapRef) {
|
|
72
|
+
// eslint-disable-next-line no-param-reassign
|
|
73
|
+
props.mapRef.current = map;
|
|
74
|
+
}
|
|
75
|
+
}, [map, props.mapRef]);
|
|
76
|
+
|
|
77
|
+
return map && createPortal(children, ctrl.getElement());
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default LayerMenuContainer;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
|
4
4
|
import {
|
|
5
5
|
bbox,
|
|
6
|
+
bboxPolygon,
|
|
7
|
+
buffer,
|
|
6
8
|
feature,
|
|
7
9
|
type FeatureCollection,
|
|
8
10
|
type GeometryCollection
|
|
@@ -16,18 +18,22 @@ import React, {
|
|
|
16
18
|
useState,
|
|
17
19
|
type Node
|
|
18
20
|
} from 'react';
|
|
19
|
-
import Map, {
|
|
21
|
+
import Map, { MapRef } from 'react-map-gl';
|
|
20
22
|
import _ from 'underscore';
|
|
21
23
|
import DrawControl from './DrawControl';
|
|
22
24
|
import './MapDraw.css';
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
26
|
+
// Override the MapboxDraw components to use MapLibre styles
|
|
27
|
+
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
|
|
28
|
+
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
|
|
29
|
+
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
|
|
29
30
|
|
|
30
31
|
type Props = {
|
|
32
|
+
/**
|
|
33
|
+
* The number of miles to buffer the GeoJSON data.
|
|
34
|
+
*/
|
|
35
|
+
buffer?: number,
|
|
36
|
+
|
|
31
37
|
/**
|
|
32
38
|
* Additional child nodes to render.
|
|
33
39
|
*/
|
|
@@ -38,8 +44,6 @@ type Props = {
|
|
|
38
44
|
*/
|
|
39
45
|
data: GeometryCollection | FeatureCollection,
|
|
40
46
|
|
|
41
|
-
layers: Array<LayerType>,
|
|
42
|
-
|
|
43
47
|
/**
|
|
44
48
|
* URL of the map style to render. This URL should contain any necessary API keys.
|
|
45
49
|
*/
|
|
@@ -55,9 +59,17 @@ type Props = {
|
|
|
55
59
|
/**
|
|
56
60
|
* Map style object.
|
|
57
61
|
*/
|
|
58
|
-
style?: any
|
|
62
|
+
style?: any,
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The time in milliseconds to zoom into the location.
|
|
66
|
+
*/
|
|
67
|
+
zoomDuration?: number
|
|
59
68
|
};
|
|
60
69
|
|
|
70
|
+
const DEFAULT_BUFFER = 2;
|
|
71
|
+
const DEFAULT_ZOOM_DELAY = 1000;
|
|
72
|
+
|
|
61
73
|
const GeometryTypes = {
|
|
62
74
|
geometryCollection: 'GeometryCollection',
|
|
63
75
|
point: 'Point'
|
|
@@ -73,8 +85,6 @@ const MapDraw = (props: Props) => {
|
|
|
73
85
|
const drawRef = useRef<MapboxDraw>();
|
|
74
86
|
const mapRef = useRef<MapRef>();
|
|
75
87
|
|
|
76
|
-
const geojsonLayers = useMemo(() => _.filter(props.layers, (layer) => !!layer.data), [props.layers]);
|
|
77
|
-
|
|
78
88
|
/**
|
|
79
89
|
* Calls the onChange prop with all of the geometries in the current drawer.
|
|
80
90
|
*
|
|
@@ -96,14 +106,24 @@ const MapDraw = (props: Props) => {
|
|
|
96
106
|
*/
|
|
97
107
|
useEffect(() => {
|
|
98
108
|
if (loaded && props.data) {
|
|
99
|
-
//
|
|
100
|
-
const
|
|
109
|
+
// Convert the GeoJSON into a bounding box
|
|
110
|
+
const box = bbox(props.data);
|
|
111
|
+
|
|
112
|
+
// Convert the bounding box to a polygon
|
|
113
|
+
const polygon = bboxPolygon(box);
|
|
114
|
+
|
|
115
|
+
// Create a buffer around the polygon
|
|
116
|
+
const polygonBuffer = buffer(polygon, props.buffer, { units: 'miles' });
|
|
101
117
|
|
|
118
|
+
// Convert the buffer to a bounding box
|
|
119
|
+
const boundingBox = bbox(polygonBuffer);
|
|
120
|
+
|
|
121
|
+
// Sets the bounding box for the current geometry.
|
|
102
122
|
if (_.every(boundingBox, _.isFinite)) {
|
|
103
123
|
const [minLng, minLat, maxLng, maxLat] = boundingBox;
|
|
104
124
|
const bounds = [[minLng, minLat], [maxLng, maxLat]];
|
|
105
125
|
|
|
106
|
-
mapRef.current.fitBounds(bounds, {
|
|
126
|
+
mapRef.current.fitBounds(bounds, { duration: props.zoomDuration });
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
// Handle special cases for geometry collection (not supported by mabox-gl-draw) and point
|
|
@@ -138,20 +158,16 @@ const MapDraw = (props: Props) => {
|
|
|
138
158
|
onCreate={onChange}
|
|
139
159
|
onUpdate={onChange}
|
|
140
160
|
onDelete={onChange}
|
|
161
|
+
position='bottom-left'
|
|
141
162
|
/>
|
|
142
|
-
{ _.map(geojsonLayers, (layer) => (
|
|
143
|
-
<Source
|
|
144
|
-
type='geojson'
|
|
145
|
-
data={layer.data}
|
|
146
|
-
>
|
|
147
|
-
<Layer
|
|
148
|
-
{..._.omit(layer, 'data')}
|
|
149
|
-
/>
|
|
150
|
-
</Source>
|
|
151
|
-
))}
|
|
152
163
|
{ props.children }
|
|
153
164
|
</Map>
|
|
154
165
|
);
|
|
155
166
|
};
|
|
156
167
|
|
|
168
|
+
MapDraw.defaultProps = {
|
|
169
|
+
buffer: DEFAULT_BUFFER,
|
|
170
|
+
zoomDuration: DEFAULT_ZOOM_DELAY
|
|
171
|
+
};
|
|
172
|
+
|
|
157
173
|
export default MapDraw;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Layer, Source } from 'react-map-gl';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
maxzoom?: number,
|
|
8
|
+
minzoom?: number,
|
|
9
|
+
opacity?: number,
|
|
10
|
+
tileSize?: number,
|
|
11
|
+
url?: string,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const RasterLayer = (props: Props) => (
|
|
15
|
+
<Source
|
|
16
|
+
tileSize={props.tileSize}
|
|
17
|
+
tiles={[props.url]}
|
|
18
|
+
type='raster'
|
|
19
|
+
>
|
|
20
|
+
<Layer
|
|
21
|
+
type='raster'
|
|
22
|
+
paint={{
|
|
23
|
+
'raster-opacity': props.opacity
|
|
24
|
+
}}
|
|
25
|
+
minzoom={props.minzoom}
|
|
26
|
+
maxzoom={props.maxzoom}
|
|
27
|
+
/>
|
|
28
|
+
</Source>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
RasterLayer.defaultProps = {
|
|
32
|
+
opacity: 0.7,
|
|
33
|
+
tileSize: 256
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default RasterLayer;
|
package/src/index.js
CHANGED
|
@@ -2,4 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Components
|
|
4
4
|
export { default as DrawControl } from './components/DrawControl';
|
|
5
|
+
export { default as GeoJsonLayer } from './components/GeoJsonLayer';
|
|
6
|
+
export { default as LayerMenu } from './components/LayerMenu';
|
|
5
7
|
export { default as MapDraw } from './components/MapDraw';
|
|
8
|
+
export { default as RasterLayer } from './components/RasterLayer';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { Layer, Source } from 'react-map-gl';
|
|
10
|
+
import _ from 'underscore';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
data?: { [key: string]: any },
|
|
14
|
+
fillStyle?: { [key: string]: any },
|
|
15
|
+
lineStyle?: { [key: string]: any },
|
|
16
|
+
pointStyle?: { [key: string]: any },
|
|
17
|
+
url?: string
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_COLOR = '#CC3333';
|
|
21
|
+
const HIGHLIGHT_COLOR = '#990000';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FILL_STYLES = {
|
|
24
|
+
'fill-color': DEFAULT_COLOR,
|
|
25
|
+
'fill-opacity': 0.2
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_LINE_STYLES = {
|
|
29
|
+
'line-color': HIGHLIGHT_COLOR,
|
|
30
|
+
'line-opacity': 0.6
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DEFAULT_POINT_STYLES = {
|
|
34
|
+
'circle-radius': [
|
|
35
|
+
'interpolate',
|
|
36
|
+
['linear'],
|
|
37
|
+
['number', ['get', 'point_count'], 1],
|
|
38
|
+
0, 4,
|
|
39
|
+
10, 14
|
|
40
|
+
],
|
|
41
|
+
'circle-stroke-width': 1,
|
|
42
|
+
'circle-color': DEFAULT_COLOR,
|
|
43
|
+
'circle-stroke-color': HIGHLIGHT_COLOR
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GeoJsonLayer = (props: Props) => {
|
|
47
|
+
const [data, setData] = useState(props.data);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the layer style for the passed style and default.
|
|
51
|
+
*
|
|
52
|
+
* @type {function(*, *): *}
|
|
53
|
+
*/
|
|
54
|
+
const getLayerStyles = useCallback((style, defaultStyle) => _.defaults(style, defaultStyle), []);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sets the fill layer style.
|
|
58
|
+
*
|
|
59
|
+
* @type {*}
|
|
60
|
+
*/
|
|
61
|
+
const fillStyle = useMemo(() => (
|
|
62
|
+
getLayerStyles(props.fillStyle, DEFAULT_FILL_STYLES)
|
|
63
|
+
), [getLayerStyles, props.fillStyle]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets the line layer style.
|
|
67
|
+
*
|
|
68
|
+
* @type {*}
|
|
69
|
+
*/
|
|
70
|
+
const lineStyle = useMemo(() => (
|
|
71
|
+
getLayerStyles(props.lineStyle, DEFAULT_LINE_STYLES)
|
|
72
|
+
), [getLayerStyles, props.lineStyle]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets the point layer style.
|
|
76
|
+
*
|
|
77
|
+
* @type {*}
|
|
78
|
+
*/
|
|
79
|
+
const pointStyle = useMemo(() => (
|
|
80
|
+
getLayerStyles(props.pointStyle, DEFAULT_POINT_STYLES)
|
|
81
|
+
), [getLayerStyles, props.pointStyle]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* If the data is passed as a URL, fetches the passed URL and sets the response on the state.
|
|
85
|
+
*/
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (props.url) {
|
|
88
|
+
fetch(props.url)
|
|
89
|
+
.then((response) => response.json())
|
|
90
|
+
.then((json) => setData(json));
|
|
91
|
+
}
|
|
92
|
+
}, [props.url]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Source
|
|
96
|
+
data={data}
|
|
97
|
+
type='geojson'
|
|
98
|
+
>
|
|
99
|
+
<Layer
|
|
100
|
+
filter={['!=', '$type', 'Point']}
|
|
101
|
+
paint={fillStyle}
|
|
102
|
+
type='fill'
|
|
103
|
+
/>
|
|
104
|
+
<Layer
|
|
105
|
+
filter={['!=', '$type', 'Point']}
|
|
106
|
+
paint={lineStyle}
|
|
107
|
+
type='line'
|
|
108
|
+
/>
|
|
109
|
+
<Layer
|
|
110
|
+
filter={['==', '$type', 'Point']}
|
|
111
|
+
paint={pointStyle}
|
|
112
|
+
type='circle'
|
|
113
|
+
/>
|
|
114
|
+
</Source>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default GeoJsonLayer;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
Children,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo, useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { BsStack } from 'react-icons/bs';
|
|
11
|
+
import { IoCheckmarkOutline } from 'react-icons/io5';
|
|
12
|
+
import _ from 'underscore';
|
|
13
|
+
import LayerMenuContainer from './LayerMenuContainer';
|
|
14
|
+
import './LayerMenu.css';
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
children: Node,
|
|
18
|
+
names: Array<string>,
|
|
19
|
+
position: 'top-left' | 'bottom-left' | 'top-right' | 'bottom-right'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MENU_PADDING = 30;
|
|
23
|
+
|
|
24
|
+
const LayerMenu = (props: Props) => {
|
|
25
|
+
const [canvasHeight, setCanvasHeight] = useState(0);
|
|
26
|
+
const [visible, setVisible] = useState();
|
|
27
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
28
|
+
|
|
29
|
+
const mapRef = useRef();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the name of the layer at the passed index.
|
|
33
|
+
*
|
|
34
|
+
* @type {unknown}
|
|
35
|
+
*/
|
|
36
|
+
const getLayerName = useCallback((index) => (
|
|
37
|
+
props.names && props.names.length > index && props.names[index]
|
|
38
|
+
), [props.names]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the child element at the passed index is visible.
|
|
42
|
+
*
|
|
43
|
+
* @type {function(*): *}
|
|
44
|
+
*/
|
|
45
|
+
const isVisible = useCallback((index) => _.includes(visible, index), [visible]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a memoized array of the child elements.
|
|
49
|
+
*
|
|
50
|
+
* @type {Array<$NonMaybeType<unknown>>}
|
|
51
|
+
*/
|
|
52
|
+
const children = useMemo(() => Children.toArray(props.children), [props.children]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a memoized array of visible child elements.
|
|
56
|
+
*/
|
|
57
|
+
const visibleChildren = useMemo(() => _.filter(children, (child, index) => isVisible(index)), [children, isVisible]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Toggles the visibility for the child element at the passed index.
|
|
61
|
+
*
|
|
62
|
+
* @type {(function(*): void)|*}
|
|
63
|
+
*/
|
|
64
|
+
const toggleVisibility = useCallback((index) => {
|
|
65
|
+
let value;
|
|
66
|
+
|
|
67
|
+
if (isVisible(index)) {
|
|
68
|
+
value = _.without(visible, index);
|
|
69
|
+
} else {
|
|
70
|
+
value = [...visible, index];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setVisible(value);
|
|
74
|
+
}, [isVisible, visible]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sets all of the child elements to be visible when the component mounts.
|
|
78
|
+
*/
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
setVisible(_.times(children.length, (index) => index));
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sets the map canvas height.
|
|
85
|
+
*/
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const { current: instance } = mapRef;
|
|
88
|
+
|
|
89
|
+
if (instance && instance._canvas) {
|
|
90
|
+
const { offsetHeight = 0 } = mapRef.current._canvas;
|
|
91
|
+
setCanvasHeight(offsetHeight);
|
|
92
|
+
}
|
|
93
|
+
}, [mapRef.current]);
|
|
94
|
+
|
|
95
|
+
if (_.isEmpty(children)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<LayerMenuContainer
|
|
102
|
+
mapRef={mapRef}
|
|
103
|
+
position={props.position}
|
|
104
|
+
>
|
|
105
|
+
<button
|
|
106
|
+
className='mapbox-gl-draw_ctrl-draw-btn layer-button'
|
|
107
|
+
onClick={() => setMenuOpen((prevMenuOpen) => !prevMenuOpen)}
|
|
108
|
+
type='button'
|
|
109
|
+
>
|
|
110
|
+
<BsStack
|
|
111
|
+
size='1.25em'
|
|
112
|
+
/>
|
|
113
|
+
</button>
|
|
114
|
+
{ menuOpen && (
|
|
115
|
+
<div
|
|
116
|
+
className='layer-menu'
|
|
117
|
+
style={{
|
|
118
|
+
maxHeight: `calc(${canvasHeight}px - ${MENU_PADDING}px)`
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className='menu'
|
|
123
|
+
>
|
|
124
|
+
{ _.map(children, (child, index) => (
|
|
125
|
+
<div
|
|
126
|
+
aria-selected={isVisible(index)}
|
|
127
|
+
className='option'
|
|
128
|
+
role='option'
|
|
129
|
+
onClick={() => toggleVisibility(index)}
|
|
130
|
+
onKeyDown={() => toggleVisibility(index)}
|
|
131
|
+
tabIndex={index}
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
className='checkmark-container'
|
|
135
|
+
>
|
|
136
|
+
{ isVisible(index) && (
|
|
137
|
+
<IoCheckmarkOutline
|
|
138
|
+
size='1em'
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
{ getLayerName(index) }
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</LayerMenuContainer>
|
|
149
|
+
{ visibleChildren }
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
LayerMenu.defaultProps = {
|
|
155
|
+
position: 'top-left'
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default LayerMenu;
|