From 7cb238efd970df91501f8948d2f35a232c34f0eb Mon Sep 17 00:00:00 2001 From: Nikolay Martyanov Date: Thu, 2 Nov 2023 16:46:00 +0100 Subject: [PATCH 1/4] front: Add Map Display for Regions. - Integrate `maplibre-gl` into the project dependencies to enable map rendering capabilities. - Implement `RegionMap` component to display the geographic boundaries of selected regions. - Modify `MainDisplay` to include the `RegionMap` component, allowing users to visually explore selected regions. Issue: #34 Signed-off-by: Nikolay Martyanov --- frontend/package.json | 1 + frontend/src/components/MainDisplay.js | 5 +- frontend/src/components/RegionMap.js | 80 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/RegionMap.js 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/components/MainDisplay.js b/frontend/src/components/MainDisplay.js index 7f6287c..4ffa78d 100644 --- a/frontend/src/components/MainDisplay.js +++ b/frontend/src/components/MainDisplay.js @@ -2,13 +2,14 @@ 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) { + if (selectedRegionId !== null && selectedRegionId !== 0) { const info = await fetchRegion(selectedRegionId); setSelectedRegionInfo(info.regionName); } @@ -20,7 +21,7 @@ const MainDisplay = () => { return (
{selectedRegionName &&

{selectedRegionName}

} - {/* Render detailed information about the selected region */} + {selectedRegionName && }
); }; diff --git a/frontend/src/components/RegionMap.js b/frontend/src/components/RegionMap.js new file mode 100644 index 0000000..f18d81c --- /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 { selectedRegionId, selectedRegionName } = useRegion(); + + const fetchSelectedRegionGeometry = async () => { + if (selectedRegionId !== null && selectedRegionId !== 0) { + const response = await fetchRegionGeometry(selectedRegionId); + 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; + } + }; + }, [selectedRegionName]); + + return
; +}; + +export default MapComponent; From 697d2d0e2c935a94dedbe0f26cf1a2f24b62263a Mon Sep 17 00:00:00 2001 From: Nikolay Martyanov Date: Thu, 2 Nov 2023 16:47:49 +0100 Subject: [PATCH 2/4] front: Implement API Endpoint for Region Geometry Data. - Introduce a new API function `fetchRegionGeometry` to retrieve the geometric data of regions necessary for map rendering. - This function handles the retrieval of polygon data for regions, enabling the new map feature to dynamically display the selected area. Signed-off-by: Nikolay Martyanov --- frontend/src/api/index.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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`); From d2239bfe3b498ebda33fc645d035476ac13fb9a0 Mon Sep 17 00:00:00 2001 From: Nikolay Martyanov Date: Thu, 2 Nov 2023 16:48:56 +0100 Subject: [PATCH 3/4] front: Improve Main Display Update on Region Selection via Breadcrumbs. - Modify `BreadcrumbNavigation` to trigger an update of the main display upon selecting a region. Previously, updates were only triggered when selecting a region from the list. - Streamline `ListOfRegions` to conditionally fetch and display subregions, enhancing the responsiveness of the user interface. - Augment `RegionContext` with `selectedRegionHasSubregions` to better manage region data and state transitions within the application. Signed-off-by: Nikolay Martyanov --- .../src/components/BreadcrumbNavigation.js | 3 ++- frontend/src/components/ListOfRegions.js | 27 ++++++++++--------- frontend/src/components/MainDisplay.js | 10 ++++--- frontend/src/components/RegionContext.js | 5 +++- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/BreadcrumbNavigation.js b/frontend/src/components/BreadcrumbNavigation.js index 9a2cb81..f95bc20 100644 --- a/frontend/src/components/BreadcrumbNavigation.js +++ b/frontend/src/components/BreadcrumbNavigation.js @@ -10,7 +10,7 @@ const BreadcrumbNavigation = () => { useEffect(() => { const fetchAndSetAncestors = async () => { - if (selectedRegionId !== null) { + if (selectedRegionId !== null && selectedRegionId !== 0) { const ancestors = await fetchAncestors(selectedRegionId); if (Array.isArray(ancestors)) { const reversedAncestors = ancestors.reverse(); @@ -28,6 +28,7 @@ const BreadcrumbNavigation = () => { const handleBreadcrumbClick = (regionId, index) => { setSelectedRegionId(regionId); + setSelectedRegionName(breadcrumbItems[index].name); // Truncate the breadcrumbItems array up to the clicked index + 1 setBreadcrumbItems(prevItems => prevItems.slice(0, index + 1)); }; diff --git a/frontend/src/components/ListOfRegions.js b/frontend/src/components/ListOfRegions.js index db1f406..9bf2ef8 100644 --- a/frontend/src/components/ListOfRegions.js +++ b/frontend/src/components/ListOfRegions.js @@ -7,37 +7,40 @@ import { useRegion } from './RegionContext'; const ListOfRegions = () => { - const { selectedRegionId, setSelectedRegionId, setSelectedRegionName } = useRegion(); + const { selectedRegionId, setSelectedRegionId, setSelectedRegionName, selectedRegionHasSubregions, setSelectedRegionHasSubregions } = 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(selectedRegionId, selectedRegionHasSubregions); + }, [selectedRegionId, setSelectedRegionHasSubregions]); - const handleItemClick = (regionId, regionName, hasSubregions) => { - if (hasSubregions) { - setSelectedRegionId(regionId); - } - setSelectedRegionName(regionName); + const handleItemClick = (region) => { + setSelectedRegionId(region.id); + setSelectedRegionName(region.name); + setSelectedRegionHasSubregions(region.hasSubregions); }; 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 4ffa78d..f7d80c9 100644 --- a/frontend/src/components/MainDisplay.js +++ b/frontend/src/components/MainDisplay.js @@ -9,9 +9,13 @@ const MainDisplay = () => { useEffect(() => { const fetchSelectedRegionInfo = async () => { - if (selectedRegionId !== null && selectedRegionId !== 0) { - const info = await fetchRegion(selectedRegionId); - setSelectedRegionInfo(info.regionName); + try { + if (selectedRegionId !== null && selectedRegionId !== 0) { + const info = await fetchRegion(selectedRegionId); + setSelectedRegionInfo(info.regionName); + } + } catch (error) { + console.error(`Error fetching region info: ${error}`); } }; diff --git a/frontend/src/components/RegionContext.js b/frontend/src/components/RegionContext.js index 529e7d3..05c40d5 100644 --- a/frontend/src/components/RegionContext.js +++ b/frontend/src/components/RegionContext.js @@ -11,6 +11,7 @@ export const RegionProvider = ({ children }) => { const [selectedRegionId, setSelectedRegionId] = useState(null); const [selectedRegionName, setSelectedRegionName] = useState(null); const [selectedRegionInfo, setSelectedRegionInfo] = useState({}); + const [selectedRegionHasSubregions, setSelectedRegionHasSubregions] = useState(false); return ( { selectedRegionName, setSelectedRegionName, selectedRegionInfo, - setSelectedRegionInfo + setSelectedRegionInfo, + selectedRegionHasSubregions, + setSelectedRegionHasSubregions, }} > {children} From 18b99d02eeae1fd58ce65389a5c79553da264f76 Mon Sep 17 00:00:00 2001 From: Nikolay Martyanov Date: Thu, 2 Nov 2023 20:24:59 +0100 Subject: [PATCH 4/4] front: Atomic Update of Region State. This commit overhauls the handling of the region state across various components, ensuring it's updated atomically. This change centralizes the state management of the selected region into a single state object within the RegionContext, replacing the previously separate state variables for selectedRegionId, selectedRegionName, and selectedRegionHasSubregions. Consequently, components like BreadcrumbNavigation, ListOfRegions, MainDisplay, and RegionMap have been updated to accommodate this new state structure. By consolidating the region state, we eliminate potential inconsistencies across components and streamline the management of the region data. Signed-off-by: Nikolay Martyanov --- .../src/components/BreadcrumbNavigation.js | 40 +++++++++++++------ frontend/src/components/ListOfRegions.js | 18 ++++++--- frontend/src/components/MainDisplay.js | 27 +++++-------- frontend/src/components/RegionContext.js | 23 ++++------- frontend/src/components/RegionMap.js | 8 ++-- 5 files changed, 60 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/BreadcrumbNavigation.js b/frontend/src/components/BreadcrumbNavigation.js index f95bc20..dc43923 100644 --- a/frontend/src/components/BreadcrumbNavigation.js +++ b/frontend/src/components/BreadcrumbNavigation.js @@ -2,33 +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 && selectedRegionId !== 0) { - 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); - setSelectedRegionName(breadcrumbItems[index].name); + 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)); }; @@ -39,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 9bf2ef8..3dfe204 100644 --- a/frontend/src/components/ListOfRegions.js +++ b/frontend/src/components/ListOfRegions.js @@ -7,7 +7,7 @@ import { useRegion } from './RegionContext'; const ListOfRegions = () => { - const { selectedRegionId, setSelectedRegionId, setSelectedRegionName, selectedRegionHasSubregions, setSelectedRegionHasSubregions } = useRegion(); + const { selectedRegion, setSelectedRegion } = useRegion(); const [regions, setRegions] = useState([]); const fetchRegions = async (regionId, hasSubregions) => { @@ -27,13 +27,19 @@ const ListOfRegions = () => { }; useEffect(() => { - fetchRegions(selectedRegionId, selectedRegionHasSubregions); - }, [selectedRegionId, setSelectedRegionHasSubregions]); + fetchRegions(selectedRegion.id, selectedRegion.hasSubregions); + }, [selectedRegion]); const handleItemClick = (region) => { - setSelectedRegionId(region.id); - setSelectedRegionName(region.name); - setSelectedRegionHasSubregions(region.hasSubregions); + setSelectedRegion( + { + id: region.id, + name: region.name, + info: selectedRegion.info, + hasSubregions: region.hasSubregions, + } + ); + }; return ( diff --git a/frontend/src/components/MainDisplay.js b/frontend/src/components/MainDisplay.js index f7d80c9..e2eeed4 100644 --- a/frontend/src/components/MainDisplay.js +++ b/frontend/src/components/MainDisplay.js @@ -5,27 +5,18 @@ import { fetchRegion } from '../api'; import RegionMap from "./RegionMap"; const MainDisplay = () => { - const { selectedRegionId, selectedRegionName, setSelectedRegionInfo } = useRegion(); - - useEffect(() => { - const fetchSelectedRegionInfo = async () => { - try { - if (selectedRegionId !== null && selectedRegionId !== 0) { - const info = await fetchRegion(selectedRegionId); - setSelectedRegionInfo(info.regionName); - } - } catch (error) { - console.error(`Error fetching region info: ${error}`); - } - }; - - fetchSelectedRegionInfo(); - }, [selectedRegionId]); + const { selectedRegion, setSelectedRegion } = useRegion(); return (
- {selectedRegionName &&

{selectedRegionName}

} - {selectedRegionName && } + {selectedRegion.name ? ( + <> +

{selectedRegion.name}

+ + + ) : ( +

No region selected.

+ )}
); }; diff --git a/frontend/src/components/RegionContext.js b/frontend/src/components/RegionContext.js index 05c40d5..163533d 100644 --- a/frontend/src/components/RegionContext.js +++ b/frontend/src/components/RegionContext.js @@ -8,24 +8,15 @@ export const useRegion = () => { }; export const RegionProvider = ({ children }) => { - const [selectedRegionId, setSelectedRegionId] = useState(null); - const [selectedRegionName, setSelectedRegionName] = useState(null); - const [selectedRegionInfo, setSelectedRegionInfo] = useState({}); - const [selectedRegionHasSubregions, setSelectedRegionHasSubregions] = useState(false); + 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 index f18d81c..fa0ef57 100644 --- a/frontend/src/components/RegionMap.js +++ b/frontend/src/components/RegionMap.js @@ -6,11 +6,11 @@ import {fetchRegionGeometry} from "../api"; const MapComponent = () => { const mapContainer = useRef(null); const map = useRef(null); - const { selectedRegionId, selectedRegionName } = useRegion(); + const { selectedRegion } = useRegion(); const fetchSelectedRegionGeometry = async () => { - if (selectedRegionId !== null && selectedRegionId !== 0) { - const response = await fetchRegionGeometry(selectedRegionId); + if (selectedRegion.id !== null && selectedRegion.id !== 0) { + const response = await fetchRegionGeometry(selectedRegion.id); if (response) { return response.geometry; } else { @@ -72,7 +72,7 @@ const MapComponent = () => { map.current = null; } }; - }, [selectedRegionName]); + }, [selectedRegion]); return
; };