Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Station groups #89

Merged
merged 11 commits into from
Oct 4, 2023
72 changes: 55 additions & 17 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { StrictMode, Suspense } from 'react';
import React, { FC, StrictMode, Suspense } from 'react';
// eslint-disable-next-line import/no-unresolved
import { createRoot } from 'react-dom/client';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
Expand All @@ -7,6 +7,8 @@ import CssBaseline from '@mui/material/CssBaseline';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';

import axios from 'axios';
import { Feature, MultiPolygon, Polygon } from '@turf/turf';
import { getData } from './store/api';
import { DataActionDispatcherContext, DataStateContext } from './store/contexts';
import { dataReducers } from './store/reducers';
Expand All @@ -15,29 +17,65 @@ import { theme } from './theme';
import routes from './routes';
import Loading from './components/Loading';

import faoAreasUrl from './files/fao_areas.geojson';
import { normalizeFaoAreaGeometry } from './components/Map/utils';

window.API_PATH = `${window.API_SERVER}/api/v1`;
window.API_FONTS = `${window.API_SERVER}/fonts`;

const App = (): JSX.Element => {
const App: FC = () => {
const [dataState, dataActionDispatcher] = React.useReducer(dataReducers, dataStateInitialValue);
const [initialized, setInitialized] = React.useState(false);

React.useEffect(function initialize() {
axios.get(faoAreasUrl).then((res) => {
const features = res.data.features as Array<
Feature<
Polygon | MultiPolygon,
{
F_AREA: string;
NAME_EN: string;
OCEAN: string;
}
>
>;
const data = features
.map<FAOArea>(({ geometry, properties: { F_AREA, NAME_EN, OCEAN } }) => ({
code: Number(F_AREA),
name: NAME_EN,
ocean: OCEAN,
geometry: normalizeFaoAreaGeometry(geometry)
}))
.sort((a, b) => a.name.localeCompare(b.name));

React.useEffect(() => {
getData<SpeciesSummary[]>(
'species/all/?order_by=matched_canonical_full_name',
(species) => {
dataActionDispatcher({ type: 'updateAllSpecies', species });
},
() => undefined
);
getData<StationSummary[]>(
'stations/all/?order_by=order',
(stations) => {
dataActionDispatcher({ type: 'updateStations', stations });
},
() => undefined
);
dataActionDispatcher({
type: 'loadFAOAreas',
faoAreas: data
});

Promise.all([
getData<StationSummary[]>(
'stations/all/?order_by=order',
(stations) => {
dataActionDispatcher({ type: 'loadStations', stations });
},
console.error
),
getData<SpeciesSummary[]>(
'species/all/?order_by=matched_canonical_full_name',
(species) => {
dataActionDispatcher({ type: 'updateAllSpecies', species });
},
console.error
)
]).then(() => {
setInitialized(true);
});
}, console.error);
}, []);

if (!initialized) return null;

return (
<StrictMode>
<Router>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Explore/LeftSidebar/InsetMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const InsetMap: FC = () => {
source: 'shadow',
type: 'fill',
paint: {
'fill-color': theme.palette.explore.main,
'fill-color': 'black',
'fill-opacity': 0.4
}
} as maplibregl.FillLayerSpecification);
Expand Down
158 changes: 158 additions & 0 deletions src/components/Explore/LeftSidebar/StationDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { useCallback, useContext } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Icon from '@mui/material/Icon';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';

import { DataActionDispatcherContext, DataStateContext } from '../../../store/contexts';
import { useStationDetails } from '../../../utils/hooks';
import DownloadButton from '../../DownloadButton';
import Loading from '../../Loading';
import TabsGroup from '../../TabsGroup';
import StationDetails from '../../Station/Details';
import StationEnvironment from '../../Station/Environment';
import StationSpecies from '../../Station/Species';
import StationText from '../../Station/Text';

const StationDetail = () => {
const dataActionDispatcher = useContext(DataActionDispatcherContext);
const { filteredStations, selectedFaoArea, selectedStation } = useContext(DataStateContext);

const selectedStationDetails = useStationDetails(selectedStation?.name);

const StationPanel = React.useCallback(
() => (selectedStationDetails ? <StationDetails station={selectedStationDetails} /> : null),
[selectedStationDetails]
);
const EnvironmentPanel = React.useCallback(
() => (selectedStationDetails ? <StationEnvironment station={selectedStationDetails} /> : null),
[selectedStationDetails]
);
const SpeciesPanel = React.useCallback(
() => (selectedStationDetails ? <StationSpecies station={selectedStationDetails} /> : null),
[selectedStationDetails]
);
const TextPanel = React.useCallback(
() => (selectedStationDetails ? <StationText station={selectedStationDetails} /> : null),
[selectedStationDetails]
);

const onNavigate = useCallback(
(navigate_to: string) => {
if (!selectedStation) return;
if (!selectedFaoArea || selectedFaoArea?.code !== selectedStation.fao_area) {
// throw '[Invalid State]: A station can only be selected after a FAO area is selected!';
return;
}
const group = filteredStations.find((g) => g.faoArea.code === selectedFaoArea.code);
if (!group) {
throw new Error('[Invalid State]: FAO area can only be selected from filtered results!');
}
const stations = group.stations;
const index = stations.findIndex((station) => station.name === selectedStation?.name);
const newIndex =
navigate_to === 'forward'
? (index + 1 + stations.length) % stations.length
: (index - 1 + stations.length) % stations.length;
dataActionDispatcher({
type: 'updateSelectedStation',
station: stations[newIndex]
});
},
[selectedStation, selectedFaoArea, filteredStations]
);

return (
<Box
sx={{
width: selectedStationDetails ? 478 : 0,
pointerEvents: 'all'
}}
>
{selectedStationDetails ? (
<Box
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
background: '#fff',
p: 1,
zIndex: 1,
boxShadow: '1px 0 5px gray'
}}
>
<Stack direction="row">
{selectedFaoArea ? (
<IconButton size="medium" sx={{ mx: 'auto' }} onClick={() => onNavigate('backward')}>
<Icon baseClassName="icons">arrow_back</Icon>
</IconButton>
) : null}
<Typography variant="h5" align="center" sx={{ mx: 'auto' }}>
Station {selectedStationDetails?.name}
</Typography>
{selectedFaoArea ? (
<IconButton size="medium" sx={{ mx: 'auto' }} onClick={() => onNavigate('forward')}>
<Icon baseClassName="icons">arrow_forward</Icon>
</IconButton>
) : null}
</Stack>
{selectedStationDetails ? (
<>
<TabsGroup
sx={{ flexGrow: 1 }}
initialPanel="Station"
panels={[
{
Panel: StationPanel,
label: 'Station'
},
{
Panel: EnvironmentPanel,
label: 'Environment'
},
{
Panel: SpeciesPanel,
label: 'Species'
},
{
Panel: TextPanel,
label: 'Text'
}
]}
/>
<Stack direction="column" spacing={2} sx={{ padding: 1 }}>
<Stack direction="row" spacing={1} justifyContent="space-between">
<Button
variant="outlined"
size="small"
onClick={() =>
dataActionDispatcher({ type: 'updateSelectedStation', station: null })
}
>
Go Back
</Button>
<DownloadButton
data={selectedStationDetails}
filename={`Station-${selectedStationDetails.name}-details`}
message="Download Data"
/>
<DownloadButton
data={selectedStationDetails.species}
filename={`Station-${selectedStationDetails.name}-Species`}
message="Download All Species"
/>
</Stack>
</Stack>
</>
) : (
<Loading />
)}
</Box>
) : null}
</Box>
);
};

export default StationDetail;
Loading
Loading