Skip to content

Commit

Permalink
MapPreview updates to support drawing & editing on map (JAM-5013) (#84)
Browse files Browse the repository at this point in the history
* Adding some new props to MapPreview to allow drawing, and then passing those down to the MapDraw component

* Update MapPreview to allow for drawing & editing, and also an option to show an empty map by default. Included new storybook story for this

* Added new MapPreviewDraw to avoid breaking changes on MapDraw

* Update MapPreview & the Draw storybook story to allow draw shape options to be customized, and pass the leafletElement with the onCreated & onEdited callbacks instead of the FeatureGroup ref which is redundant/already available

* Allow JSX objects to be passed for labels in FormInput & InputButtons for more custom labels

* Add shapeOptions prop to MapPreview to allow customizing of shape styles, etc

* add disableEdit prop to MapPreview, rename disableEdit to disableDraw so we can allow edit specifically to be disabled

* Add key to EditControl so it re-renders when the edit prop changes

---------

Co-authored-by: Ben Adams <[email protected]>
  • Loading branch information
beadams86 and Ben Adams authored Nov 7, 2023
1 parent 60b82da commit 50318d4
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/components/FormInput/FormInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const FormInput = (props) => {

FormInput.propTypes = {
className: PropTypes.string,
label: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
type: PropTypes.string,
id: PropTypes.string,
name: PropTypes.string,
Expand Down
2 changes: 1 addition & 1 deletion src/components/InputButton/InputButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ InputButton.propTypes = {
icon: PropTypes.oneOfType([PropTypes.element, PropTypes.array]),
forwardedRef: PropTypes.object,
id: PropTypes.string,
label: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
value: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
Expand Down
69 changes: 62 additions & 7 deletions src/components/MapPreview/MapPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {

import Map from '../Map';
import Marker from '../MapMarker';
import MapDraw from '../MapDraw';
import MapPreviewDraw from '../MapPreviewDraw';

const logger = new Logger('FormInput', {
isBrowser: true
Expand All @@ -34,10 +34,18 @@ const MapPreview = ({
projection,
fetchLayerData,
fitGeoJson = true,
label = "Area of Interest",
label = 'Area of Interest',
showGeometryType = true,
shapeOptions,
mapRef,
useMapEffect
useMapEffect,
disableDraw = true,
disableEdit = false,
drawControlOptions,
onDrawCreated,
onDrawEdited,
featureRef = useRef(),
emptyMap = false,
}) => {
const layers = useLayers(availableLayers, fetchLayerData);

Expand Down Expand Up @@ -137,12 +145,51 @@ const MapPreview = ({
useMapEffect
};

/**
* handleOnDraw
* @description Fires when a draw layer is created. Triggers callback if available.
* Also clears all any previous layers except the newly created one.
*/
function handleOnDraw (drawLayer) {
const drawnItems = featureRef.current?.leafletElement._layers;
if (Object.keys(drawnItems).length > 1) {
Object.keys(drawnItems).forEach((layerid, index) => {
if (index > 0) return;
const layer = drawnItems[layerid];
featureRef.current.leafletElement.removeLayer(layer);
});
}
const { leafletElement } = featureRef.current || {};
if (typeof onDrawCreated === 'function') {
onDrawCreated(drawLayer, leafletElement);
}
}

/**
* handleOnEditDraw
* @description Fires after editing an existing draw layer. Triggers callback if available
*/
function handleOnEditDraw (drawLayer) {
const { leafletElement } = featureRef.current || {};
if (typeof onDrawEdited === 'function') {
onDrawEdited(drawLayer, leafletElement);
}
}

return (
<LayersContext.Provider value={{ ...layers }}>
<figure className="map-preview">
<Map {...mapSettings}>
<MapDraw disableEditControls={true}>
{features.map((feature) => {
<MapPreviewDraw
disableDrawControls={disableDraw}
disableEditControls={disableEdit}
onCreated={handleOnDraw}
onEdited={handleOnEditDraw}
featureRef={featureRef}
controlOptions={drawControlOptions}
shapeOptions={shapeOptions}
>
{!emptyMap && features.map((feature) => {
const { geometry, properties } = feature;

const {
Expand Down Expand Up @@ -201,7 +248,7 @@ const MapPreview = ({

return null;
})}
</MapDraw>
</MapPreviewDraw>
</Map>
<figcaption className="map-preview-header">
<h5>{label}</h5>
Expand Down Expand Up @@ -265,7 +312,15 @@ MapPreview.propTypes = {
label: PropTypes.string,
showGeometryType: PropTypes.bool,
mapRef: PropTypes.object,
useMapEffect: PropTypes.func
useMapEffect: PropTypes.func,
shapeOptions: PropTypes.object,
disableDraw: PropTypes.bool,
disableEdit: PropTypes.bool,
drawControlOptions: PropTypes.object,
onDrawCreated: PropTypes.func,
onDrawEdited: PropTypes.func,
featureRef: PropTypes.object,
emptyMap: PropTypes.bool
};

export default MapPreview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useRef } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Story from '../../../../stories/helpers/Story';

import MapPreview from '..';

const STORY_COMPONENT = 'Map Preview';
const STORY_NAME = 'Empty Map & Draw Controls';

const stories = storiesOf(`Components/${STORY_COMPONENT}`, module);

stories.add(STORY_NAME, () => {
const featureRef = useRef();

function handleOnDraw (drawLayer, leafletElement) {
action(`${STORY_COMPONENT}::onDraw`)(drawLayer, leafletElement);
}

function handleOnEdit (drawLayer, leafletElement) {
action(`${STORY_COMPONENT}::onEdit`)(drawLayer, leafletElement);
}

return (
<Story component={STORY_COMPONENT} name={STORY_NAME}>
<MapPreview
center={{
lat: 38.8048,
lng: -77.0469
}}
emptyMap={true}
fitGeoJson={true}
displayAccessRequests={false}
displayAOIDetails={false}
disableDraw={false}
disableEdit={false}
onDrawCreated={handleOnDraw}
onDrawEdited={handleOnEdit}
featureRef={featureRef}
drawControlOptions={{
circle: true,
circlemarker: false,
marker: false,
polygon: true,
polyline: false,
rectangle: true
}}
/>
</Story>
);
});
147 changes: 147 additions & 0 deletions src/components/MapPreviewDraw/MapPreviewDraw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'leaflet-draw/dist/leaflet.draw.css';
import { Popup } from 'react-leaflet';
import { EditControl } from 'react-leaflet-draw';

import { useMapMarkerIcon } from '../../hooks';

import FeatureGroup from '../FeatureGroup';

const DEFAULT_CONTROL_OPTIONS = {
circle: false,
circlemarker: false,
marker: true,
polygon: true,
polyline: false,
rectangle: true
};

const SHAPE_CONTROLS = ['circle', 'polygon', 'polyline', 'rectangle'];

const DEFAULT_SHAPE_OPTIONS = {
opacity: 1,
weight: 3
};

const MapPreviewDraw = ({
children,
forwardedRef,
onCreated,
onEdited,
disableDrawControls = false,
disableEditControls = false,
controlOptions,
shapeOptions,
PopupContent,
featureGroup,
featureRef,
...rest
}) => {
const { icon } = useMapMarkerIcon();

const markerOptions = {
marker: {
icon
}
};

const drawOptions = {
...DEFAULT_CONTROL_OPTIONS,
...markerOptions,
...controlOptions
};

// Loop through all of our configured options and determine the
// shape configuration for each if a valid shape

Object.keys(drawOptions).forEach((key) => {
// Check if the option is turned off or if it's a valid shape

if (!drawOptions[key] || !SHAPE_CONTROLS.includes(key)) return;

// If it's set to true, we want to initialize the object for the shape
// to set our options on

if (drawOptions[key] === true) {
drawOptions[key] = {};
}

drawOptions[key].shapeOptions = {
...DEFAULT_SHAPE_OPTIONS,
...drawOptions[key].shapeOptions,
...(shapeOptions && shapeOptions.style)
};
});

/**
* handleOnCreated
* @description Fires when a layer is created. Triggers callback if available.
* Clears all other layers except the newly created one.
*/

function handleOnCreated ({ layer } = {}) {
if (typeof onCreated === 'function') {
onCreated(layer, forwardedRef);
}
}

/**
* handleOnEdited
* @description Fires when a layer is edited. Triggers callback if available.
*/

function handleOnEdited ({ target } = {}) {
if (typeof onCreated === 'function') {
onEdited(target, forwardedRef);
}
}

return (
<FeatureGroup featureGroup={featureGroup} ref={featureRef}>
{children}
{!disableDrawControls && (
<>
<EditControl
key={disableEditControls}
position="bottomright"
onCreated={handleOnCreated}
onEdited={handleOnEdited}
draw={drawOptions}
edit={{
edit: !disableEditControls,
remove: false
}}
/>
{PopupContent && (
<Popup>
<PopupContent {...rest} />
</Popup>
)}
</>
)}
</FeatureGroup>
);
};

MapPreviewDraw.propTypes = {
children: PropTypes.node,
forwardedRef: PropTypes.object,
onCreated: PropTypes.func,
onEdited: PropTypes.func,
disableDrawControls: PropTypes.bool,
disableEditControls: PropTypes.bool,
controlOptions: PropTypes.object,
shapeOptions: PropTypes.object,
featureGroup: PropTypes.object,
PopupContent: PropTypes.any,
featureRef: PropTypes.object
};

const MapPreviewDrawWithRefs = React.forwardRef(function mapDraw (props, ref) {
return <MapPreviewDraw {...props} forwardedRef={ref} />;
});

MapPreviewDrawWithRefs.displayName = 'MapDrawWithRefs';

export default MapPreviewDrawWithRefs;
51 changes: 51 additions & 0 deletions src/components/MapPreviewDraw/MapPreviewDraw.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { shallow } from 'enzyme';

import MapPreviewDraw from '.';

describe('MapDraw', () => {
describe('Render', () => {
const testClass = 'test';
const testText = 'Hi';

const mapdraw = shallow(
<MapPreviewDraw>
<div className={testClass}>{testText}</div>
</MapPreviewDraw>
);
const mapdrawDive = mapdraw.dive();
const editcontrol = mapdrawDive.find('ForwardRef(Leaflet(EditControl))');

it('should render with the position prop', () => {
expect(editcontrol.prop('position')).toEqual('bottomright');
});

it('should render with the disabled shape features', () => {
expect(editcontrol.prop('draw').circle).toEqual(false);
expect(editcontrol.prop('draw').circlemarker).toEqual(false);
expect(editcontrol.prop('draw').polyline).toEqual(false);
});

it('should render children within the component', () => {
expect(mapdraw.find(`.${testClass}`).text()).toEqual(testText);
});
});

describe('Events', () => {
const mapdraw = shallow(<MapPreviewDraw onCreated={handleOnCreated} />);
const mapdrawDive = mapdraw.dive();
const editcontrol = mapdrawDive.find('ForwardRef(Leaflet(EditControl))');

let testCreated = 1;

function handleOnCreated () {
testCreated++;
}

editcontrol.prop('onCreated')();

it('should fire given onCreated event', () => {
expect(testCreated).toEqual(2);
});
});
});
1 change: 1 addition & 0 deletions src/components/MapPreviewDraw/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './MapPreviewDraw';

0 comments on commit 50318d4

Please sign in to comment.