diff --git a/frontend/package.json b/frontend/package.json index 2aa97fa..ca78273 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" }, diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 810adef..a0b0592 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -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}`); + } +} + export const fetchAncestors = async (regionId) => { try { const response = await api.get(`/api/regions/${regionId}/ancestors`); diff --git a/frontend/src/components/BreadcrumbNavigation.js b/frontend/src/components/BreadcrumbNavigation.js index 9a2cb81..dc43923 100644 --- a/frontend/src/components/BreadcrumbNavigation.js +++ b/frontend/src/components/BreadcrumbNavigation.js @@ -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)); }; @@ -38,7 +55,7 @@ const BreadcrumbNavigation = () => { handleBreadcrumbClick(item.id, index)} + onClick={() => handleBreadcrumbClick(item.id, index, item.hasSubregions)} style={{ cursor: 'pointer' }} > {item.name} diff --git a/frontend/src/components/ListOfRegions.js b/frontend/src/components/ListOfRegions.js index db1f406..3dfe204 100644 --- a/frontend/src/components/ListOfRegions.js +++ b/frontend/src/components/ListOfRegions.js @@ -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 ( {regions.map((region) => ( - handleItemClick(region.id, region.name, region.hasSubregions)}> + handleItemClick(region)}> {region.name} ))} diff --git a/frontend/src/components/MainDisplay.js b/frontend/src/components/MainDisplay.js index 7f6287c..e2eeed4 100644 --- a/frontend/src/components/MainDisplay.js +++ b/frontend/src/components/MainDisplay.js @@ -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 (
- {selectedRegionName &&

{selectedRegionName}

} - {/* Render detailed information about the selected region */} + {selectedRegion.name ? ( + <> +

{selectedRegion.name}

+ + + ) : ( +

No region selected.

+ )}
); }; diff --git a/frontend/src/components/RegionContext.js b/frontend/src/components/RegionContext.js index 529e7d3..163533d 100644 --- a/frontend/src/components/RegionContext.js +++ b/frontend/src/components/RegionContext.js @@ -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 ( - + {children} ); diff --git a/frontend/src/components/RegionMap.js b/frontend/src/components/RegionMap.js new file mode 100644 index 0000000..fa0ef57 --- /dev/null +++ b/frontend/src/components/RegionMap.js @@ -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; + } + + 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
; +}; + +export default MapComponent;