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

Region Visualization with Map Integration #49

Merged
merged 4 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.13",
"axios": "^1.5.1",
"maplibre-gl": "^3.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ export const fetchRegion = async (regionId) => {
}
}

// Fetch the geometry for a region. Returns null if no geometry is found.
export const fetchRegionGeometry = async (regionId, force) => {
try {
const response = await api.get(`/api/regions/${regionId}/geometry`, {params: {resolveEmpty: force}});
if (response.status === 204 || response.status === 404) {
return null;
}
return response.data;
} catch (error) {
console.error('Error fetching region geometry:', error);
throw new Error(`Error fetching region geometry: ${error.message}`);
}
}
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved

export const fetchAncestors = async (regionId) => {
try {
const response = await api.get(`/api/regions/${regionId}/ancestors`);
Expand Down
39 changes: 28 additions & 11 deletions frontend/src/components/BreadcrumbNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,49 @@ import React, { useEffect, useState } from 'react';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import { Typography } from "@mui/material";
import { useRegion } from './RegionContext';
import { fetchAncestors } from '../api';
import {fetchAncestors, fetchRegion} from '../api';

const BreadcrumbNavigation = () => {
const { setSelectedRegionId, setSelectedRegionName, selectedRegionId, selectedRegionName } = useRegion();
const [breadcrumbItems, setBreadcrumbItems] = useState([{ id: null, name: 'World' }]);
const { selectedRegion, setSelectedRegion } = useRegion();
const [breadcrumbItems, setBreadcrumbItems] = useState([{ id: null, name: 'World', hasSubregions: true }]);

useEffect(() => {
const fetchAndSetAncestors = async () => {
if (selectedRegionId !== null) {
const ancestors = await fetchAncestors(selectedRegionId);
if (selectedRegion.id !== null && selectedRegion.id !== 0) {
const ancestors = await fetchAncestors(selectedRegion.id);
if (Array.isArray(ancestors)) {
const reversedAncestors = ancestors.reverse();
setBreadcrumbItems([{id: 0, name: 'World'}, ...reversedAncestors]);
setBreadcrumbItems([{id: 0, name: 'World', hasSubregions: true }, ...reversedAncestors]);
} else {
console.error('Ancestors is not an array:', ancestors);
}
} else {
setBreadcrumbItems([{ id: null, name: 'World' }]);
setBreadcrumbItems([{ id: null, name: 'World', hasSubregions: true }]);
}
};
fetchAndSetAncestors();
}, [selectedRegionId]);
}, [selectedRegion]);


const handleBreadcrumbClick = (regionId, index) => {
setSelectedRegionId(regionId);
const handleBreadcrumbClick = async (regionId, index) => {

let hasSubregions;
if (regionId === null || regionId === 0 ) {
hasSubregions = true;
} else {
try {
const region = await fetchRegion(regionId);
hasSubregions = region.hasSubregions;
} catch (error) {
console.error(`Error fetching region ${regionId}, consider the region as not having subregions:`, error);
hasSubregions = false;
}
}
setSelectedRegion({
id: regionId,
name: breadcrumbItems[index].name,
hasSubregions: hasSubregions,
});
// Truncate the breadcrumbItems array up to the clicked index + 1
setBreadcrumbItems(prevItems => prevItems.slice(0, index + 1));
};
Expand All @@ -38,7 +55,7 @@ const BreadcrumbNavigation = () => {
<Typography
color="inherit"
key={index}
onClick={() => handleBreadcrumbClick(item.id, index)}
onClick={() => handleBreadcrumbClick(item.id, index, item.hasSubregions)}
style={{ cursor: 'pointer' }}
>
{item.name}
Expand Down
33 changes: 21 additions & 12 deletions frontend/src/components/ListOfRegions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,46 @@ import { useRegion } from './RegionContext';

const ListOfRegions = () => {

const { selectedRegionId, setSelectedRegionId, setSelectedRegionName } = useRegion();
const { selectedRegion, setSelectedRegion } = useRegion();
const [regions, setRegions] = useState([]);

const fetchRegions = async (regionId) => {
const fetchRegions = async (regionId, hasSubregions) => {

let newRegions = [];
if (regionId) {
newRegions = await fetchSubregions(regionId);
if (hasSubregions) {
newRegions = await fetchSubregions(regionId);
}
} else {
newRegions = await fetchRootRegions();
}

setRegions(newRegions);
if (newRegions.length > 0) {
setRegions(newRegions);
}
};

useEffect(() => {
fetchRegions(selectedRegionId).then(r => console.log(r));
}, [selectedRegionId]);
fetchRegions(selectedRegion.id, selectedRegion.hasSubregions);
}, [selectedRegion]);

const handleItemClick = (region) => {
setSelectedRegion(
{
id: region.id,
name: region.name,
info: selectedRegion.info,
hasSubregions: region.hasSubregions,
}
);

const handleItemClick = (regionId, regionName, hasSubregions) => {
if (hasSubregions) {
setSelectedRegionId(regionId);
}
setSelectedRegionName(regionName);
};

return (
<Box style={{ height: '400px', overflow: 'auto' }}>
<List>
{regions.map((region) => (
<ListItem key={region.id} button onClick={() => handleItemClick(region.id, region.name, region.hasSubregions)}>
<ListItem key={region.id} button onClick={() => handleItemClick(region)}>
{region.name}
</ListItem>
))}
Expand Down
24 changes: 10 additions & 14 deletions frontend/src/components/MainDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,21 @@
import React, { useEffect } from 'react';
import { useRegion } from './RegionContext';
import { fetchRegion } from '../api';
import RegionMap from "./RegionMap";

const MainDisplay = () => {
const { selectedRegionId, selectedRegionName, setSelectedRegionInfo } = useRegion();

useEffect(() => {
const fetchSelectedRegionInfo = async () => {
if (selectedRegionId !== null) {
const info = await fetchRegion(selectedRegionId);
setSelectedRegionInfo(info.regionName);
}
};

fetchSelectedRegionInfo();
}, [selectedRegionId]);
const { selectedRegion, setSelectedRegion } = useRegion();

return (
<div>
{selectedRegionName && <h1>{selectedRegionName}</h1>}
{/* Render detailed information about the selected region */}
{selectedRegion.name ? (
<>
<h1>{selectedRegion.name}</h1>
<RegionMap/>
</>
) : (
<p>No region selected.</p>
)}
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved
</div>
);
};
Expand Down
20 changes: 7 additions & 13 deletions frontend/src/components/RegionContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,15 @@ export const useRegion = () => {
};

export const RegionProvider = ({ children }) => {
const [selectedRegionId, setSelectedRegionId] = useState(null);
const [selectedRegionName, setSelectedRegionName] = useState(null);
const [selectedRegionInfo, setSelectedRegionInfo] = useState({});
const [selectedRegion, setSelectedRegion] = useState({
id: null,
name: 'World',
info: {},
hasSubregions: false,
});

return (
<RegionContext.Provider
value={{
selectedRegionId,
setSelectedRegionId,
selectedRegionName,
setSelectedRegionName,
selectedRegionInfo,
setSelectedRegionInfo
}}
>
<RegionContext.Provider value={{ selectedRegion, setSelectedRegion }}>
{children}
</RegionContext.Provider>
);
Expand Down
80 changes: 80 additions & 0 deletions frontend/src/components/RegionMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, {useEffect, useRef} from 'react';
import maplibregl from 'maplibre-gl';
import {useRegion} from "./RegionContext";
import {fetchRegionGeometry} from "../api";

const MapComponent = () => {
const mapContainer = useRef(null);
const map = useRef(null);
const { selectedRegion } = useRegion();

const fetchSelectedRegionGeometry = async () => {
if (selectedRegion.id !== null && selectedRegion.id !== 0) {
const response = await fetchRegionGeometry(selectedRegion.id);
if (response) {
return response.geometry;
} else {
return null;
}
}
}

useEffect(() => {
if (map.current) return;

const initializeMap = async () => {
const polygonData = await fetchSelectedRegionGeometry();

if (!polygonData || !polygonData.coordinates) {
// Handle the case where there is no geometry data, perhaps set a default view?
console.log('No geometry data available for the selected region.');
return;
}
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved

map.current = new maplibregl.Map({
container: mapContainer.current,
style: 'https://demotiles.maplibre.org/style.json', // specify the base map style
center: [
polygonData.coordinates[0][0][0][0],
polygonData.coordinates[0][0][0][1]
], // center the map on the first coordinate of the polygon
zoom: 9
});

map.current.on('load', () => {
map.current.addSource('polygon', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: polygonData // use the geometry from the API response
}
});

map.current.addLayer({
id: 'polygon',
type: 'fill',
source: 'polygon',
layout: {},
paint: {
'fill-color': '#088', // fill color of the polygon
'fill-opacity': 0.8
}
});
});
};

initializeMap().then(r => console.log(r));

return () => {
if (map.current) {
map.current.remove();
map.current = null;
}
};
}, [selectedRegion]);

return <div ref={mapContainer} style={{ width: '100%', height: '400px' }} />;
};

export default MapComponent;
Loading