Skip to content

Commit

Permalink
Merge pull request #248 from performant-software/feature/cdc113_place…
Browse files Browse the repository at this point in the history
…_layers

CDC #113 - Place Layers
  • Loading branch information
dleadbetter authored Feb 1, 2024
2 parents ab85a27 + b961d58 commit 764fda1
Show file tree
Hide file tree
Showing 20 changed files with 3,519 additions and 47 deletions.
6 changes: 3 additions & 3 deletions packages/controlled-vocabulary/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/controlled-vocabulary",
"version": "1.1.2",
"version": "1.1.3",
"description": "A package of components to allow user to configure dropdown elements. Use with the \"controlled_vocabulary\" gem.",
"license": "MIT",
"main": "./build/index.js",
Expand All @@ -12,8 +12,8 @@
"build": "webpack --mode production && flow-copy-source -v src types"
},
"dependencies": {
"@performant-software/semantic-components": "^1.1.2",
"@performant-software/shared-components": "^1.1.2",
"@performant-software/semantic-components": "^1.1.3",
"@performant-software/shared-components": "^1.1.3",
"i18next": "^21.9.2",
"semantic-ui-react": "^2.1.2",
"underscore": "^1.13.2"
Expand Down
5 changes: 3 additions & 2 deletions packages/geospatial/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/geospatial",
"version": "1.1.2",
"version": "1.1.3",
"description": "TODO: ADD",
"license": "MIT",
"main": "./build/index.js",
Expand All @@ -12,7 +12,8 @@
"@mapbox/mapbox-gl-draw": "^1.4.3",
"@turf/turf": "^6.5.0",
"mapbox-gl": "npm:[email protected]",
"maplibre-gl": "^3.5.2",
"maplibre-gl": "^3.6.2",
"react-icons": "^5.0.1",
"react-map-gl": "^7.1.6",
"underscore": "^1.13.6"
},
Expand Down
118 changes: 118 additions & 0 deletions packages/geospatial/src/components/GeoJsonLayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// @flow

import React, {
useCallback,
useEffect,
useMemo,
useState
} from 'react';
import { Layer, Source } from 'react-map-gl';
import _ from 'underscore';

type Props = {
data?: { [key: string]: any },
fillStyle?: { [key: string]: any },
lineStyle?: { [key: string]: any },
pointStyle?: { [key: string]: any },
url?: string
};

const DEFAULT_COLOR = '#CC3333';
const HIGHLIGHT_COLOR = '#990000';

const DEFAULT_FILL_STYLES = {
'fill-color': DEFAULT_COLOR,
'fill-opacity': 0.2
};

const DEFAULT_LINE_STYLES = {
'line-color': HIGHLIGHT_COLOR,
'line-opacity': 0.6
};

const DEFAULT_POINT_STYLES = {
'circle-radius': [
'interpolate',
['linear'],
['number', ['get', 'point_count'], 1],
0, 4,
10, 14
],
'circle-stroke-width': 1,
'circle-color': DEFAULT_COLOR,
'circle-stroke-color': HIGHLIGHT_COLOR
};

const GeoJsonLayer = (props: Props) => {
const [data, setData] = useState(props.data);

/**
* Returns the layer style for the passed style and default.
*
* @type {function(*, *): *}
*/
const getLayerStyles = useCallback((style, defaultStyle) => _.defaults(style, defaultStyle), []);

/**
* Sets the fill layer style.
*
* @type {*}
*/
const fillStyle = useMemo(() => (
getLayerStyles(props.fillStyle, DEFAULT_FILL_STYLES)
), [getLayerStyles, props.fillStyle]);

/**
* Sets the line layer style.
*
* @type {*}
*/
const lineStyle = useMemo(() => (
getLayerStyles(props.lineStyle, DEFAULT_LINE_STYLES)
), [getLayerStyles, props.lineStyle]);

/**
* Sets the point layer style.
*
* @type {*}
*/
const pointStyle = useMemo(() => (
getLayerStyles(props.pointStyle, DEFAULT_POINT_STYLES)
), [getLayerStyles, props.pointStyle]);

/**
* If the data is passed as a URL, fetches the passed URL and sets the response on the state.
*/
useEffect(() => {
if (props.url) {
fetch(props.url)
.then((response) => response.json())
.then((json) => setData(json));
}
}, [props.url]);

return (
<Source
data={data}
type='geojson'
>
<Layer
filter={['!=', '$type', 'Point']}
paint={fillStyle}
type='fill'
/>
<Layer
filter={['!=', '$type', 'Point']}
paint={lineStyle}
type='line'
/>
<Layer
filter={['==', '$type', 'Point']}
paint={pointStyle}
type='circle'
/>
</Source>
);
};

export default GeoJsonLayer;
38 changes: 38 additions & 0 deletions packages/geospatial/src/components/LayerMenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.maplibregl-ctrl-group.maplibregl-ctrl button.layer-button.mapbox-gl-draw_ctrl-draw-btn {
display: flex;
align-items: center;
justify-content: center;
color: #000000;
}

.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu {
background-color: #FFFFFF;
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 2px;
font-family: 'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif;
position: absolute;
top: 0px;
left: 40px;
overflow: auto;
width: max-content;
}

.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option {
cursor: pointer;
padding: 0.7em 2em 0.7em 2em;
pointer-events: all;
display: flex;
align-items: center;
}

.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option:hover {
background-color: rgba(0, 0, 0, .05);
}

.maplibregl-ctrl-group.maplibregl-ctrl .layer-menu > .menu > .option > .checkmark-container {
color: #009E60;
display: flex;
align-items: center;
height: 100%;
width: 20px;
}
158 changes: 158 additions & 0 deletions packages/geospatial/src/components/LayerMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// @flow

import React, {
Children,
useCallback,
useEffect,
useMemo, useRef,
useState
} from 'react';
import { BsStack } from 'react-icons/bs';
import { IoCheckmarkOutline } from 'react-icons/io5';
import _ from 'underscore';
import MapControl from './MapControl';
import './LayerMenu.css';

type Props = {
children: Node,
names: Array<string>,
position: 'top-left' | 'bottom-left' | 'top-right' | 'bottom-right'
};

const MENU_PADDING = 30;

const LayerMenu = (props: Props) => {
const [canvasHeight, setCanvasHeight] = useState(0);
const [visible, setVisible] = useState();
const [menuOpen, setMenuOpen] = useState(false);

const mapRef = useRef();

/**
* Returns the name of the layer at the passed index.
*
* @type {unknown}
*/
const getLayerName = useCallback((index) => (
props.names && props.names.length > index && props.names[index]
), [props.names]);

/**
* Returns true if the child element at the passed index is visible.
*
* @type {function(*): *}
*/
const isVisible = useCallback((index) => _.includes(visible, index), [visible]);

/**
* Returns a memoized array of the child elements.
*
* @type {Array<$NonMaybeType<unknown>>}
*/
const children = useMemo(() => Children.toArray(props.children), [props.children]);

/**
* Returns a memoized array of visible child elements.
*/
const visibleChildren = useMemo(() => _.filter(children, (child, index) => isVisible(index)), [children, isVisible]);

/**
* Toggles the visibility for the child element at the passed index.
*
* @type {(function(*): void)|*}
*/
const toggleVisibility = useCallback((index) => {
let value;

if (isVisible(index)) {
value = _.without(visible, index);
} else {
value = [...visible, index];
}

setVisible(value);
}, [isVisible, visible]);

/**
* Sets all of the child elements to be visible when the component mounts.
*/
useEffect(() => {
setVisible(_.times(children.length, (index) => index));
}, []);

/**
* Sets the map canvas height.
*/
useEffect(() => {
const { current: instance } = mapRef;

if (instance && instance._canvas) {
const { offsetHeight = 0 } = mapRef.current._canvas;
setCanvasHeight(offsetHeight);
}
}, [mapRef.current]);

if (_.isEmpty(children)) {
return null;
}

return (
<>
<MapControl
mapRef={mapRef}
position={props.position}
>
<button
className='mapbox-gl-draw_ctrl-draw-btn layer-button'
onClick={() => setMenuOpen((prevMenuOpen) => !prevMenuOpen)}
type='button'
>
<BsStack
size='1.25em'
/>
</button>
{ menuOpen && (
<div
className='layer-menu'
style={{
maxHeight: `calc(${canvasHeight}px - ${MENU_PADDING}px)`
}}
>
<div
className='menu'
>
{ _.map(children, (child, index) => (
<div
aria-selected={isVisible(index)}
className='option'
role='option'
onClick={() => toggleVisibility(index)}
onKeyDown={() => toggleVisibility(index)}
tabIndex={index}
>
<div
className='checkmark-container'
>
{ isVisible(index) && (
<IoCheckmarkOutline
size='1em'
/>
)}
</div>
{ getLayerName(index) }
</div>
))}
</div>
</div>
)}
</MapControl>
{ visibleChildren }
</>
);
};

LayerMenu.defaultProps = {
position: 'top-left'
};

export default LayerMenu;
Loading

0 comments on commit 764fda1

Please sign in to comment.