Skip to content

Commit

Permalink
feat: Explorable map of landscapes (complete feature) (#448)
Browse files Browse the repository at this point in the history
* chore: Explore Landscapes base branch

* feat: Added map to landscapes directory (#449)

* feat: Explorable map of landscapes: Display points and clusters (#455)

* feat: Explorable map of landscapes - Pin popup (#458)
  • Loading branch information
josebui authored Jul 21, 2022
1 parent 0a5e99e commit aeed05f
Show file tree
Hide file tree
Showing 15 changed files with 302 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ on:
push:
branches:
- main
- feature/restrict-membership
- feature/explore-landscapes
pull_request:
branches:
- main
- feature/restrict-membership
- feature/explore-landscapes

jobs:
lint:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
pull_request:
branches:
- main
- feature/restrict-membership
- feature/explore-landscapes
types:
- opened
- edited
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ name: "CodeQL"

on:
push:
branches: [ main, feature/restrict-membership ]
branches: [ main, feature/explore-landscapes ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main, feature/restrict-membership ]
branches: [ main, feature/explore-landscapes ]
schedule:
- cron: '34 6 * * 4'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/localization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ on:
push:
branches:
- main
- feature/restrict-membership
- feature/explore-landscapes
pull_request:
branches:
- main
- feature/restrict-membership
- feature/explore-landscapes

jobs:
missing-keys:
Expand Down
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"react-hook-form": "^7.28.1",
"react-i18next": "^11.16.2",
"react-leaflet": "^3.2.5",
"react-leaflet-markercluster": "^3.0.0-rc1",
"react-redux": "^7.2.6",
"react-router-dom": "^6.2.2",
"uuid": "^8.3.2",
Expand Down
23 changes: 23 additions & 0 deletions src/landscape/components/LandscapeList.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import LandscapeMemberLeave from 'landscape/membership/components/LandscapeMembe

import { withProps } from 'react-hoc';

import LandscapeListMap from './LandscapeListMap';

import theme from 'theme';

const MemberLeaveButton = withProps(LandscapeMemberLeave, {
Expand Down Expand Up @@ -143,6 +145,27 @@ const LandscapeList = () => {
marginTop: theme.spacing(2),
}}
></Typography>
<Stack
component="section"
aria-label={t('landscape.list_map_section_label')}
spacing={2}
sx={{ mb: 4 }}
>
<LandscapeListMap />
<Trans i18nKey="landscape.list_map_help">
<Typography>
Prefix
<Link component={RouterLink} to={`/landscapes/new`}>
add link
</Link>
or
<Link href={t('landscape.list_map_help_url')} target="_blank">
help
</Link>
.
</Typography>
</Trans>
</Stack>
<TableResponsive
columns={columns}
rows={landscapes}
Expand Down
26 changes: 26 additions & 0 deletions src/landscape/components/LandscapeList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import React from 'react';

import _ from 'lodash/fp';
import { act } from 'react-dom/test-utils';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import { useSearchParams } from 'react-router-dom';

import useMediaQuery from '@mui/material/useMediaQuery';

import LandscapeList from 'landscape/components/LandscapeList';
import * as terrasoApi from 'terrasoBackend/api';

const GEOJSON =
'{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[-80.02098083496094, 0.8184536092473124], [-80.04364013671875, 0.8177670337355836], [-80.04844665527342, 0.8184536092473124], [-80.04981994628906, 0.8260059320976082], [-80.07247924804686, 0.802662342941431], [-80.09170532226562, 0.779318620539376], [-80.10063171386719, 0.7532284249372649], [-80.09857177734375, 0.7223319390984623], [-80.09307861328125, 0.7140928403610857], [-80.10337829589842, 0.6955548144696846], [-80.09788513183594, 0.6742703246919985], [-80.08827209472656, 0.6488661346824502], [-80.07797241210938, 0.6495527361122139], [-80.06561279296875, 0.6522991408974699], [-80.06235122680664, 0.6468063298344634], [-80.02098083496094, 0.8184536092473124]]]}, "properties": {}}]}';

// Omit console error for DataGrid issue: https://github.com/mui/mui-x/issues/3850
global.console.error = jest.fn();

jest.mock('terrasoBackend/api');
jest.mock('react-leaflet-markercluster', () => jest.fn());

jest.mock('@mui/material/useMediaQuery');

Expand All @@ -24,6 +29,9 @@ jest.mock('react-router-dom', () => ({
}));

const setup = async initialState => {
// TODO Improve testing to test clusters functionality
MarkerClusterGroup.mockImplementation(({ children }) => <>{children}</>);

await render(<LandscapeList />, {
account: {
hasToken: true,
Expand Down Expand Up @@ -70,6 +78,7 @@ const baseListTest = async () => {
description: 'Landscape Description',
website: 'www.landscape.org',
location: 'Ecuador, Quito',
areaPolygon: GEOJSON,
defaultGroup: {
edges: [
{
Expand Down Expand Up @@ -104,6 +113,23 @@ const baseListTest = async () => {
expect(
screen.getByRole('heading', { name: 'Landscapes' })
).toBeInTheDocument();

// Map
const mapRegion = screen.getByRole('region', {
name: 'Landscapes map',
});
expect(mapRegion).toBeInTheDocument();

const markers = within(mapRegion).getAllByRole('button');
expect(markers.length).toBe(17); // 15 + zoom buttons

await act(async () => fireEvent.click(markers[0]));

within(mapRegion).getByRole('link', {
name: 'View details about Landscape Name 0',
});

// Table
const rows = screen.getAllByRole('row');
expect(rows.length).toBe(16); // 15 displayed + header
expect(
Expand Down
39 changes: 39 additions & 0 deletions src/landscape/components/LandscapeListMap.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.landscape-list-map-cluster-icon div {
background-clip: padding-box;
background-color: #985329;
width: 30px;
height: 25px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
padding-top: 5px;
color: #fff;
}

.landscape-list-map-cluster-icon {
background-clip: padding-box;
background-color: #98532999;
border-radius: 20px;
}

.landscape-list-map-marker-icon {
background-clip: padding-box;
background-color: #985329;
border-radius: 30px;
}

.landscape-marker-popup .leaflet-popup-content-wrapper {
background: #fff;
font-size: 16px;
line-height: 24px;
border-radius: 3px;
border: 1px solid #985329;
box-shadow: none;
}
.landscape-marker-popup .leaflet-popup-tip {
border: 1px solid transparent;
border-right: 1px solid #985329;
background-color: transparent;
box-shadow: none;
}
115 changes: 115 additions & 0 deletions src/landscape/components/LandscapeListMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useEffect, useMemo, useRef } from 'react';

import L from 'leaflet';
import _ from 'lodash/fp';

import Map from 'gis/components/Map';

import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';

import { Marker, Popup, useMap } from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import { useSelector } from 'react-redux';

import { getLandscapePin } from 'landscape/landscapeUtils';

import './LandscapeListMap.css';

import { useTranslation } from 'react-i18next';
import { Link as RouterLink } from 'react-router-dom';

import { Link, Typography } from '@mui/material';

import { countryNameForCode } from 'common/utils';

const LandscapesClusters = () => {
const map = useMap();
const { t } = useTranslation();
const { landscapes } = useSelector(state => state.landscape.list);

const clusterRef = useRef();

const landscapesWithPosition = useMemo(() => {
return landscapes
.map(landscape => ({
position: getLandscapePin(landscape),
data: landscape,
}))
.filter(landscape => !!landscape.position);
}, [landscapes]);

useEffect(() => {
if (!_.isEmpty(landscapesWithPosition)) {
const bounds = clusterRef.current?.getBounds?.();
if (bounds) {
map.fitBounds(bounds);
}
}
}, [map, landscapesWithPosition]);

return (
<MarkerClusterGroup
ref={clusterRef}
maxClusterRadius={40}
showCoverageOnHover={false}
iconCreateFunction={cluster => {
return L.divIcon({
className: 'landscape-list-map-cluster-icon',
iconSize: new L.Point(40, 40),
html: `<div>${cluster.getChildCount()}</div>`,
});
}}
>
{landscapesWithPosition.map((landscape, index) => (
<Marker
icon={L.divIcon({
className: 'landscape-list-map-marker-icon',
iconSize: new L.Point(15, 15),
html: `<span class="visually-hidden">${landscape.data.name}</span>`,
})}
key={index}
position={landscape.position}
>
<Popup className="landscape-marker-popup" closeButton={false}>
<Link
variant="h6"
component={RouterLink}
to={`/landscapes/${landscape.data.slug}`}
>
{landscape.data.name}
</Link>
<Typography variant="caption" display="block" sx={{ mb: 1 }}>
{countryNameForCode(landscape.data.location)?.name ||
landscape.data.location}
</Typography>
<Link
variant="body2"
component={RouterLink}
to={`/landscapes/${landscape.data.slug}`}
>
{t('landscape.list_map_popup_link', {
name: landscape.data.name,
})}
</Link>
</Popup>
</Marker>
))}
</MarkerClusterGroup>
);
};

const LandscapeListMap = () => {
return (
<Map
style={{
width: '100%',
height: '400px',
}}
>
<LandscapesClusters />
</Map>
);
};

export default LandscapeListMap;
Loading

0 comments on commit aeed05f

Please sign in to comment.