Skip to content

Commit 764fda1

Browse files
authored
Merge pull request #248 from performant-software/feature/cdc113_place_layers
CDC #113 - Place Layers
2 parents ab85a27 + b961d58 commit 764fda1

File tree

20 files changed

+3519
-47
lines changed

20 files changed

+3519
-47
lines changed

packages/controlled-vocabulary/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@performant-software/controlled-vocabulary",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "A package of components to allow user to configure dropdown elements. Use with the \"controlled_vocabulary\" gem.",
55
"license": "MIT",
66
"main": "./build/index.js",
@@ -12,8 +12,8 @@
1212
"build": "webpack --mode production && flow-copy-source -v src types"
1313
},
1414
"dependencies": {
15-
"@performant-software/semantic-components": "^1.1.2",
16-
"@performant-software/shared-components": "^1.1.2",
15+
"@performant-software/semantic-components": "^1.1.3",
16+
"@performant-software/shared-components": "^1.1.3",
1717
"i18next": "^21.9.2",
1818
"semantic-ui-react": "^2.1.2",
1919
"underscore": "^1.13.2"

packages/geospatial/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@performant-software/geospatial",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "TODO: ADD",
55
"license": "MIT",
66
"main": "./build/index.js",
@@ -12,7 +12,8 @@
1212
"@mapbox/mapbox-gl-draw": "^1.4.3",
1313
"@turf/turf": "^6.5.0",
1414
"mapbox-gl": "npm:[email protected]",
15-
"maplibre-gl": "^3.5.2",
15+
"maplibre-gl": "^3.6.2",
16+
"react-icons": "^5.0.1",
1617
"react-map-gl": "^7.1.6",
1718
"underscore": "^1.13.6"
1819
},
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.maplibregl-ctrl-group.maplibregl-ctrl button.layer-button.mapbox-gl-draw_ctrl-draw-btn {
2+
display: flex;
3+
align-items: center;
4+
justify-content: center;
5+
color: #000000;
6+
}
7+
8+
.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu {
9+
background-color: #FFFFFF;
10+
border-radius: 4px;
11+
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 2px;
12+
font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
13+
position: absolute;
14+
top: 0px;
15+
left: 40px;
16+
overflow: auto;
17+
width: max-content;
18+
}
19+
20+
.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option {
21+
cursor: pointer;
22+
padding: 0.7em 2em 0.7em 2em;
23+
pointer-events: all;
24+
display: flex;
25+
align-items: center;
26+
}
27+
28+
.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option:hover {
29+
background-color: rgba(0, 0, 0, .05);
30+
}
31+
32+
.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option > .checkmark-container {
33+
color: #009E60;
34+
display: flex;
35+
align-items: center;
36+
height: 100%;
37+
width: 20px;
38+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 MapControl from './MapControl';
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+
<MapControl
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+
</MapControl>
149+
{ visibleChildren }
150+
</>
151+
);
152+
};
153+
154+
LayerMenu.defaultProps = {
155+
position: 'top-left'
156+
};
157+
158+
export default LayerMenu;

0 commit comments

Comments
 (0)