Skip to content

Commit

Permalink
Allow users to see existing projects' AOI during project creation
Browse files Browse the repository at this point in the history
  • Loading branch information
willemarcel authored and d-rita committed Jun 18, 2021
1 parent acc6480 commit e45318e
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 8 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/basemapMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const BasemapMenu = ({ map }) => {
};

return (
<div className="bg-white blue-dark flex mt2 ml2 f7 br1 shadow-1">
<div className="bg-white blue-dark flex mt2 ml2 f7 fr br1 shadow-1">
{styles.map((style, k) => {
return (
<div
Expand Down
19 changes: 14 additions & 5 deletions frontend/src/components/formInputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,25 @@ export function UserCountrySelect({ className }: Object) {
);
}

export const CheckBoxInput = ({ isActive, changeState, className = '' }) => (
export const CheckBoxInput = ({ isActive, changeState, className = '', disabled }) => (
<div
role="checkbox"
disabled={disabled}
aria-checked={isActive}
onClick={changeState}
onKeyPress={changeState}
onClick={disabled ? () => {} : changeState}
onKeyPress={disabled ? () => {} : changeState}
tabIndex="0"
className={`bg-white w1 h1 ma1 ba bw1 b--red br1 relative pointer ${className}`}
className={`bg-white w1 h1 ma1 ba bw1 ${
disabled ? 'b--grey-light' : 'b--red'
} br1 relative pointer ${className}`}
>
{isActive ? <div className="bg-red ba b--white bw1 br1 w-100 h-100"></div> : <></>}
{isActive ? (
<div
className={`${disabled ? 'bg-grey-light' : 'bg-red'} ba b--white bw1 br1 w-100 h-100`}
></div>
) : (
<></>
)}
</div>
);

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/projectCreate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const ProjectCreate = (props) => {
const intl = useIntl();
const token = useSelector((state) => state.auth.get('token'));
const [drawModeIsActive, setDrawModeIsActive] = useState(false);
const [showProjectsAOILayer, setShowProjectsAOILayer] = useState(false);

const setDataGeom = (geom, display) => {
mapObj.map.fitBounds(bbox(geom), { padding: 200 });
Expand Down Expand Up @@ -243,6 +244,8 @@ const ProjectCreate = (props) => {
drawHandler={drawHandler}
deleteHandler={deleteHandler}
drawIsActive={drawModeIsActive}
showProjectsAOILayer={showProjectsAOILayer}
setShowProjectsAOILayer={setShowProjectsAOILayer}
/>
);
case 2:
Expand Down Expand Up @@ -279,6 +282,7 @@ const ProjectCreate = (props) => {
setMapObj={setMapObj}
step={step}
uploadFile={uploadFile}
showProjectsAOILayer={showProjectsAOILayer}
/>
</Suspense>
<div className="cf absolute bg-white o-90 top-1 left-1 pa3 mw6">
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/components/projectCreate/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ export default defineMessages({
id: 'management.projects.create.reset.button',
defaultMessage: 'Reset',
},
showProjectsAOILayer: {
id: 'management.projects.create.show_aois',
defaultMessage: 'Show existing projects',
},
disabledAOILayer: {
id: 'management.projects.create.show_aois.disabled',
defaultMessage:
"Zoom in to be able to activate the visualization of other projects' areas of interest.",
},
enableAOILayer: {
id: 'management.projects.create.show_aois.enable',
defaultMessage: "Enable the visualization of the existing projects' areas of interest.",
},
colorLegend: {
id: 'management.projects.create.show_aois.legend',
defaultMessage: 'Color legend:',
},
taskNumberMessage: {
id: 'management.projects.create.split.tasks.number',
defaultMessage: 'A new project will be created with {n} tasks.',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/projectCreate/navButtons.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const NavButtons = (props) => {
? props.metadata.geom.features.length
: props.metadata.taskGrid.features.length,
});
// clear the otherProjects source before passing to step 2
props.mapObj.map.getSource('otherProjects').setData(featureCollection([]));
}

break;
Expand Down
101 changes: 99 additions & 2 deletions frontend/src/components/projectCreate/projectCreationMap.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useLayoutEffect } from 'react';
import React, { useLayoutEffect, useEffect, useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
Expand All @@ -8,8 +8,17 @@ import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { useDropzone } from 'react-dropzone';

import { MAPBOX_TOKEN, MAP_STYLE, CHART_COLOURS, MAPBOX_RTL_PLUGIN_URL } from '../../config';
import {
MAPBOX_TOKEN,
MAP_STYLE,
CHART_COLOURS,
MAPBOX_RTL_PLUGIN_URL,
TASK_COLOURS,
} from '../../config';
import { fetchLocalJSONAPI } from '../../network/genericJSONRequest';
import { useDebouncedCallback } from '../../hooks/UseThrottle';
import { BasemapMenu } from '../basemapMenu';
import { ProjectsAOILayerCheckBox } from './projectsAOILayerCheckBox';

mapboxgl.accessToken = MAPBOX_TOKEN;
try {
Expand All @@ -21,11 +30,40 @@ try {
const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, uploadFile }) => {
const mapRef = React.createRef();
const locale = useSelector((state) => state.preferences['locale']);
const token = useSelector((state) => state.auth.get('token'));
const [showProjectsAOILayer, setShowProjectsAOILayer] = useState(false);
const [aoiCanBeActivated, setAOICanBeActivated] = useState(false);
const [debouncedGetProjectsAOI] = useDebouncedCallback(() => getProjectsAOI(), 1500);
const { getRootProps, getInputProps } = useDropzone({
onDrop: step === 1 ? uploadFile : () => {}, // drag&drop is activated only on the first step
noClick: true,
noKeyboard: true,
});
const minZoomLevelToAOIVisualization = 11;

const getProjectsAOI = () => {
if (aoiCanBeActivated && showProjectsAOILayer && step === 1) {
let bounds = mapObj.map.getBounds();
let bbox = `${bounds._sw.lng},${bounds._sw.lat},${bounds._ne.lng},${bounds._ne.lat}`;
fetchLocalJSONAPI(`projects/queries/bbox/?bbox=${bbox}&srid=4326`, token).then((res) =>
mapObj.map.getSource('otherProjects').setData(res),
);
}
};

const clearProjectsAOI = useCallback(() => {
if (mapObj && mapObj.map && mapObj.map.getSource('otherProjects')) {
mapObj.map.getSource('otherProjects').setData(featureCollection([]));
}
}, [mapObj]);

useEffect(() => {
if (showProjectsAOILayer && step === 1) {
debouncedGetProjectsAOI();
} else {
clearProjectsAOI();
}
}, [showProjectsAOILayer, debouncedGetProjectsAOI, clearProjectsAOI, step]);

useLayoutEffect(() => {
const map = new mapboxgl.Map({
Expand Down Expand Up @@ -109,8 +147,52 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step,
},
});
}
if (map.getSource('otherProjects') === undefined) {
const colorByStatus = [
'match',
['get', 'projectStatus'],
'DRAFT',
TASK_COLOURS.MAPPED,
'PUBLISHED',
TASK_COLOURS.VALIDATED,
'ARCHIVED',
TASK_COLOURS.BADIMAGERY,
'rgba(0,0,0,0)', // fallback option required by mapbox-gl
];
map.addSource('otherProjects', {
type: 'geojson',
data: featureCollection([]),
});
map.addLayer({
id: 'otherProjectsLine',
type: 'line',
source: 'otherProjects',
paint: {
'line-color': colorByStatus,
'line-width': 2,
'line-opacity': 1,
},
});
map.addLayer({
id: 'otherProjectsFill',
type: 'fill',
source: 'otherProjects',
paint: {
'fill-color': colorByStatus,
'fill-opacity': ['match', ['get', 'projectStatus'], 'PUBLISHED', 0.1, 0.3],
},
});
}
};

useLayoutEffect(() => {
if (mapObj.map !== null) {
mapObj.map.on('moveend', (event) => {
debouncedGetProjectsAOI();
});
}
});

useLayoutEffect(() => {
if (mapObj.map !== null) {
mapObj.map.on('load', () => {
Expand All @@ -123,6 +205,14 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step,
mapObj.map.on('draw.delete', (event) => {
updateMetadata({ ...metadata, geom: null, area: 0 });
});
// enable disable the project AOI visualization checkbox
mapObj.map.on('zoomend', (event) => {
if (mapObj.map.getZoom() < minZoomLevelToAOIVisualization) {
setAOICanBeActivated(false);
} else {
setAOICanBeActivated(true);
}
});

mapObj.map.on('style.load', (event) => {
if (!MAPBOX_TOKEN) {
Expand All @@ -147,6 +237,13 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step,
return (
<div className="w-100 h-100-l relative" {...getRootProps()}>
<div className="absolute top-0 right-0 z-5 mr2">
{step === 1 && (
<ProjectsAOILayerCheckBox
isActive={showProjectsAOILayer}
setActive={setShowProjectsAOILayer}
disabled={!aoiCanBeActivated}
/>
)}
<BasemapMenu map={mapObj.map} />
<input className="dn" {...getInputProps()} />
</div>
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/components/projectCreate/projectsAOILayerCheckBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { CheckBoxInput } from '../formInputs';
import { FormattedMessage } from 'react-intl';
import ReactTooltip from 'react-tooltip';

import messages from './messages';
import statusMessages from '../projectDetail/messages';
import { TASK_COLOURS } from '../../config';

export const ProjectsAOILayerCheckBox = ({ isActive, setActive, disabled }) => {
return (
<>
<div className="bg-white fl dib pv1 ph2 blue-dark mt2 mh2 f7 br1 shadow-1">
<CheckBoxInput
isActive={isActive}
disabled={disabled}
changeState={() => setActive(!isActive)}
className="dib mr2 v-mid"
/>
<span className="di v-mid" data-tip>
<FormattedMessage {...messages.showProjectsAOILayer} />
</span>
<ReactTooltip place="bottom">
{disabled ? (
<FormattedMessage {...messages.disabledAOILayer} />
) : (
<div>
<div className="db">
<FormattedMessage {...messages.enableAOILayer} />
</div>
<div className="db pt2 pb1">
<FormattedMessage {...messages.colorLegend} />
</div>
<div className="db">
<ProjectStatusLegend color={TASK_COLOURS.VALIDATED} />
<FormattedMessage {...statusMessages.status_PUBLISHED} />
</div>
<div className="db">
<ProjectStatusLegend color={TASK_COLOURS.MAPPED} />
<FormattedMessage {...statusMessages.status_DRAFT} />
</div>
<div className="db">
<ProjectStatusLegend color={TASK_COLOURS.BADIMAGERY} />
<FormattedMessage {...statusMessages.status_ARCHIVED} />
</div>
</div>
)}
</ReactTooltip>
</div>
</>
);
};

const ProjectStatusLegend = ({ color }) => (
<span style={{ backgroundColor: color }} className="h1 w1 dib mr2"></span>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';

import { ProjectsAOILayerCheckBox } from '../projectsAOILayerCheckBox';
import { IntlProviders } from '../../../utils/testWithIntl';

describe('ProjectsAOILayerCheckBox', () => {
const testFn = jest.fn();
it('with disabled property', () => {
render(
<IntlProviders>
<ProjectsAOILayerCheckBox isActive={false} setActive={testFn} disabled={true} />
</IntlProviders>,
);
expect(screen.getByText('Show existing projects')).toBeInTheDocument();
expect(screen.getByRole('checkbox').className).toContain('b--grey-light');
expect(screen.getByRole('checkbox').className).not.toContain('b--red');
userEvent.hover(screen.getByText('Show existing projects'));
expect(
screen.getByText(
"Zoom in to be able to activate the visualization of other projects' areas of interest.",
),
).toBeInTheDocument();
userEvent.click(screen.getByRole('checkbox'));
expect(testFn).not.toHaveBeenCalled();
});
it('with disabled=false property', () => {
render(
<IntlProviders>
<ProjectsAOILayerCheckBox isActive={false} setActive={testFn} disabled={false} />
</IntlProviders>,
);
expect(screen.getByText('Show existing projects')).toBeInTheDocument();
expect(screen.getByRole('checkbox').className).not.toContain('b--grey-light');
expect(screen.getByRole('checkbox').className).toContain('b--red');
userEvent.hover(screen.getByText('Show existing projects'));
expect(
screen.getByText("Enable the visualization of the existing projects' areas of interest."),
).toBeInTheDocument();
userEvent.click(screen.getByRole('checkbox'));
expect(testFn).toHaveBeenCalled();
});
});
4 changes: 4 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@
"management.projects.create.errors.fileSize": "We only accept files up to {fileSize} MB. Please reduce the size of your file and try again.",
"management.projects.create.split_task.description": "Make tasks smaller by clicking on specific tasks or drawing an area on the map.",
"management.projects.create.reset.button": "Reset",
"management.projects.create.show_aois": "Show existing projects",
"management.projects.create.show_aois.disabled": "Zoom in to be able to activate the visualization of other projects' areas of interest.",
"management.projects.create.show_aois.enable": "Enable the visualization of the existing projects' areas of interest.",
"management.projects.create.show_aois.legend": "Color legend:",
"management.projects.create.split.tasks.number": "A new project will be created with {n} tasks.",
"management.projects.create.split.tasks.area": "The size of each task is approximately {area} km{sq}.",
"management.projects.create.split_task.draw.button": "Draw area to split",
Expand Down

0 comments on commit e45318e

Please sign in to comment.