Skip to content

Commit fb86fcf

Browse files
committed
form: enabling support for multiple geometry selection mode (Closes #30)
1 parent 7710d98 commit fb86fcf

File tree

8 files changed

+261
-52
lines changed

8 files changed

+261
-52
lines changed

src/lib/components/form/fields/GeometryField/GeometryField.stories.js

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const Template = (args) => (
4949
/**
5050
* Component stories
5151
*/
52-
export const Basic = Template.bind({});
53-
Basic.args = {
52+
export const MultipleLayers = Template.bind({});
53+
MultipleLayers.args = {
5454
fieldPath: 'geometry',
5555
label: 'Geometry',
5656
labelIcon: 'location arrow',
@@ -66,4 +66,89 @@ Basic.args = {
6666
});
6767
}, 100);
6868
},
69+
interactiveMapConfig: {
70+
mapConfig: {
71+
mapContainer: {
72+
center: [30, -50],
73+
zoom: 1,
74+
zoomControl: true,
75+
},
76+
geometryEditorConfig: {
77+
toolbarConfig: {
78+
positions: {
79+
draw: 'topleft',
80+
edit: 'topright',
81+
},
82+
drawText: false,
83+
drawCircleMarker: false,
84+
drawCircle: false,
85+
cutPolygon: false,
86+
controlOrder: [
87+
'drawMarker',
88+
'drawPolyline',
89+
'drawRectangle',
90+
'drawPolygon',
91+
'editMode',
92+
'dragMode',
93+
'cutPolygon',
94+
'rotateMode',
95+
'removalMode',
96+
],
97+
},
98+
uniqueLayer: false,
99+
},
100+
},
101+
},
102+
};
103+
104+
export const UniqueGeometry = Template.bind({});
105+
UniqueGeometry.args = {
106+
fieldPath: 'geometry',
107+
label: 'Geometry',
108+
labelIcon: 'location arrow',
109+
onDataClean: () => {
110+
setTimeout((_) => {
111+
toast({
112+
title: 'Data cleaned',
113+
description: 'Data has been cleaned',
114+
type: 'info',
115+
icon: 'info',
116+
time: 5000,
117+
animation: 'fade right',
118+
});
119+
}, 100);
120+
},
121+
interactiveMapConfig: {
122+
mapConfig: {
123+
mapContainer: {
124+
center: [30, -50],
125+
zoom: 1,
126+
zoomControl: true,
127+
},
128+
geometryEditorConfig: {
129+
toolbarConfig: {
130+
positions: {
131+
draw: 'topleft',
132+
edit: 'topright',
133+
},
134+
drawText: false,
135+
drawCircleMarker: false,
136+
drawCircle: false,
137+
cutPolygon: false,
138+
controlOrder: [
139+
'drawMarker',
140+
'drawPolyline',
141+
'drawRectangle',
142+
'drawPolygon',
143+
'editMode',
144+
'dragMode',
145+
'cutPolygon',
146+
'rotateMode',
147+
'removalMode',
148+
],
149+
},
150+
uniqueLayer: true,
151+
},
152+
},
153+
},
69154
};

src/lib/components/form/fields/GeometryField/InteractiveMap/GeometryEditor.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import PropTypes from 'prop-types';
1111

1212
import { MapEventHandler } from './MapEventHandler';
1313

14+
import { LayerLoader } from './LayerLoader';
15+
1416
import { useDrawEvents } from '../../../../../base';
15-
import { GeometryEditorControl, LayerLoader } from '../../../../layers';
17+
import { GeometryEditorControl } from '../../../../layers';
1618

1719
/**
1820
* Geometry editor component.
@@ -25,20 +27,42 @@ import { GeometryEditorControl, LayerLoader } from '../../../../layers';
2527
*/
2628
export const GeometryEditor = ({ geometryStore, geometryEditorConfig }) => {
2729
// States
30+
const [renderFlag, setRenderFlag] = useState(null);
2831
const [controlRendered, setControlRendered] = useState(false);
2932

33+
// Configurations
34+
const { uniqueLayer } = geometryEditorConfig;
35+
36+
// Auxiliary functions
37+
const renderFlagGenerator = (id) => {
38+
uniqueLayer ? setRenderFlag(id) : null;
39+
};
40+
3041
// Handlers
31-
const mapEventHandler = new MapEventHandler(geometryStore);
42+
const mapEventHandler = new MapEventHandler(
43+
geometryStore,
44+
renderFlagGenerator,
45+
geometryEditorConfig
46+
);
3247

3348
// Hooks
3449
useDrawEvents(mapEventHandler, () => setControlRendered(true));
3550

51+
// Checking if the current store is already populated.
52+
if (!geometryStore.isEmpty() && renderFlag === null) {
53+
setRenderFlag(geometryStore.indexKey);
54+
}
55+
3656
return (
3757
<>
3858
{controlRendered ? (
3959
<>
4060
<GeometryEditorControl {...geometryEditorConfig} />
41-
<LayerLoader layers={geometryStore.getLayers()} />
61+
<LayerLoader
62+
renderFlag={renderFlag}
63+
layers={geometryStore.getLayers()}
64+
enableMultipleLayers={!geometryEditorConfig.uniqueLayer}
65+
/>
4266
</>
4367
) : null}
4468
</>
@@ -47,7 +71,10 @@ export const GeometryEditor = ({ geometryStore, geometryEditorConfig }) => {
4771

4872
GeometryEditor.propTypes = {
4973
geometryStore: PropTypes.object.isRequired,
50-
geometryEditorConfig: PropTypes.object,
74+
geometryEditorConfig: PropTypes.shape({
75+
toolbarConfig: PropTypes.object.isRequired,
76+
uniqueLayer: PropTypes.bool.isRequired,
77+
}),
5178
};
5279

5380
GeometryEditor.defaultProps = {
@@ -73,5 +100,6 @@ GeometryEditor.defaultProps = {
73100
'removalMode',
74101
],
75102
},
103+
uniqueLayer: false,
76104
},
77105
};

src/lib/components/form/fields/GeometryField/InteractiveMap/InteractiveMap.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import React from 'react';
1010
import PropTypes from 'prop-types';
1111

12+
import _get from 'lodash/get';
13+
1214
import { MapContainer } from 'react-leaflet';
1315

1416
import { GeometryStore } from '../GeometryStore';
@@ -40,10 +42,16 @@ const InteractiveMapComponent = ({ geometryStore, mapConfig }) => {
4042
* rendering system makes all the content modification.
4143
*
4244
*/
43-
export const InteractiveMap = React.memo(
44-
InteractiveMapComponent,
45-
(props) => true // avoiding re-render
46-
);
45+
export const InteractiveMap = React.memo(InteractiveMapComponent, (props) => {
46+
// We need to define if the map must be rerender (in react) or not.
47+
// In this case, we are defining if rerender is required based on
48+
// the mode selected by the user:
49+
// - uniqueLayer (true): Unique will be presented in the map. In this case,
50+
// we need rerender to sync the map and the formik store;
51+
// - uniqueLayer (false): All drawn layers will be presented, and we do not
52+
// need to rerender.
53+
return !_get(props, 'mapConfig.geometryEditorConfig.uniqueLayer');
54+
});
4755

4856
InteractiveMap.propTypes = {
4957
geometryStore: PropTypes.object.isRequired,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* This file is part of GEO-Metadata-Previewer.
3+
* Copyright (C) 2022 GEO Secretariat.
4+
*
5+
* GEO-Metadata-Previewer is free software; you can redistribute it and/or modify it
6+
* under the terms of the MIT License; see LICENSE file for more details.
7+
*/
8+
9+
import React, { useEffect, useRef } from 'react';
10+
import PropTypes from 'prop-types';
11+
12+
import _last from 'lodash/last';
13+
14+
import { useLeafletContext } from '@react-leaflet/core';
15+
16+
/**
17+
* Component to load multiple layers on a Leaflet Container (`Layer` or `Map` instance).
18+
* @constructor
19+
*
20+
* @param {Array} layers Array of `Leaflet.Layer` to be added in the `Container`.
21+
* @param {Number} renderFlag Flag to mark the current render step.
22+
* @param {Boolean} enableMultipleLayers Flag to enable/disable the multiple layer usage.
23+
*
24+
* @returns {JSX.Element}
25+
*/
26+
export const LayerLoader = ({ layers, renderFlag, enableMultipleLayers }) => {
27+
const context = useLeafletContext();
28+
const renderFlagRef = useRef(null);
29+
30+
// ToDo: Review if the "multi effect" approach is a good solution.
31+
if (enableMultipleLayers) {
32+
useEffect(() => {
33+
const container = context.map;
34+
35+
layers.forEach((layer) => layer.addTo(container));
36+
37+
return () => {
38+
layers.forEach((layer) => container.removeLayer(layer));
39+
};
40+
}, []);
41+
} else {
42+
useEffect(() => {
43+
const container = context.map;
44+
45+
if (renderFlagRef.current === renderFlag) {
46+
return;
47+
}
48+
49+
// Removing all drawn layers in the map
50+
const drawedLayers = container.pm.getGeomanDrawLayers();
51+
52+
drawedLayers.forEach((layer) => {
53+
container.removeLayer(layer);
54+
});
55+
56+
// Selecting one layer to draw in the map
57+
const layerToDraw = _last(layers);
58+
59+
if (layerToDraw) {
60+
layerToDraw.addTo(container);
61+
62+
renderFlagRef.current = renderFlag;
63+
64+
return () => {
65+
container.removeLayer(layerToDraw);
66+
};
67+
}
68+
}, [renderFlag]);
69+
}
70+
71+
return null;
72+
};
73+
74+
LayerLoader.propTypes = {
75+
layers: PropTypes.array.isRequired,
76+
renderFlag: PropTypes.number,
77+
enableMultipleLayers: PropTypes.bool.isRequired,
78+
};
79+
80+
LayerLoader.defaultProps = {};

src/lib/components/layers/base/LayerLoader.test.js renamed to src/lib/components/form/fields/GeometryField/InteractiveMap/LayerLoader.test.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,21 @@ import { renderWithMapContainer } from '@tests/setup';
1313

1414
describe('LayerLoader tests', () => {
1515
describe('Render tests', () => {
16-
it('should render without crashing', () => {
16+
it('should render without crashing with enableMultipleLayer disabled', () => {
1717
renderWithMapContainer(
1818
<>
19-
<LayerLoader layers={[]} />
19+
<LayerLoader
20+
layers={[]}
21+
renderFlag={1}
22+
enableMultipleLayers={false}
23+
/>
24+
</>
25+
);
26+
});
27+
it('should render without crashing enableMultipleLayer enabled', () => {
28+
renderWithMapContainer(
29+
<>
30+
<LayerLoader layers={[]} renderFlag={1} enableMultipleLayers={true} />
2031
</>
2132
);
2233
});

0 commit comments

Comments
 (0)