From 9e40701844e6aa1e3675583968da5a139cf80429 Mon Sep 17 00:00:00 2001 From: THS-LMIS <91137979+THS-LMIS@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:13:39 +0100 Subject: [PATCH] Anlegen einer Subsite mit dem Titel des entsprechenden Gruppennamens (#17) Co-authored-by: Thomas Schley --- app/components/Breadcrumbs.module.css | 10 ++ app/components/Breadcrumbs.tsx | 21 ++- app/components/Footer.module.css | 4 + app/components/Header.module.css | 5 + app/components/OpenLayers.tsx | 48 +++++- .../SingleSelectionGroupContents.tsx | 57 ++++++ app/components/SingleSelectionGroups.tsx | 13 +- app/models/Sensor.tsx | 1 + app/models/SensorResponse.tsx | 3 +- app/routes.ts | 3 +- app/routes/Group.tsx | 163 ++++++++++++++++++ app/routes/Home.module.css | 14 +- app/routes/Home.tsx | 4 + app/services/fiwareService.tsx | 31 +++- 14 files changed, 357 insertions(+), 20 deletions(-) create mode 100644 app/components/SingleSelectionGroupContents.tsx create mode 100644 app/routes/Group.tsx diff --git a/app/components/Breadcrumbs.module.css b/app/components/Breadcrumbs.module.css index c810c07..1fe18cf 100644 --- a/app/components/Breadcrumbs.module.css +++ b/app/components/Breadcrumbs.module.css @@ -2,9 +2,16 @@ background-color: #41485A; display: flex; padding: 10px min(4vh, 2vw); + @media only screen and (max-height: 767px) { + padding-bottom: 5px; + padding-top: 5px; + } a { color: white; font-size: large; + @media only screen and (max-height: 767px) { + font-size: medium; + } text-decoration: none; } a + a:before { @@ -15,4 +22,7 @@ a.active { font-weight: bold; } + a.disabled { + pointer-events: none; + } } \ No newline at end of file diff --git a/app/components/Breadcrumbs.tsx b/app/components/Breadcrumbs.tsx index 5e07a65..10d093c 100644 --- a/app/components/Breadcrumbs.tsx +++ b/app/components/Breadcrumbs.tsx @@ -2,9 +2,25 @@ import {NavLink, useLocation} from 'react-router'; import styles from './Breadcrumbs.module.css'; const routeLabels: { [key: string]: string } = { + group: 'Gruppe', impressum: 'Impressum' }; +const routeWithoutLink: string[] = [ + 'group' +]; + +function calculateNavLinkClassNames(isActive: boolean, disabled: boolean) { + const classNames: string[] = []; + if (isActive) { + classNames.push(styles.active); + } + if (disabled) { + classNames.push(styles.disabled); + } + return classNames.join(' '); +} + function Breadcrumbs() { const location = useLocation(); const pathnames = location.pathname.split('/').filter((x) => x); @@ -20,9 +36,10 @@ function Breadcrumbs() { {pathnames.map((pathname, index) => { const to = `/${pathnames.slice(0, index + 1).join('/')}`; const label: string = routeLabels[pathname] || pathname; + const disabled = routeWithoutLink.includes(pathname); return (isActive ? styles.active : undefined)} + to={disabled ? '#' : to} + className={({ isActive }) => calculateNavLinkClassNames(isActive, disabled)} key={pathname} > {decodeURIComponent(label)} diff --git a/app/components/Footer.module.css b/app/components/Footer.module.css index 760e115..85b735e 100644 --- a/app/components/Footer.module.css +++ b/app/components/Footer.module.css @@ -5,6 +5,10 @@ gap: 40px; list-style: none; padding: 20px min(4vh, 2vw); + @media only screen and (max-height: 767px) { + padding-bottom: 10px; + padding-top: 10px; + } a { color: white; font-size: x-large; diff --git a/app/components/Header.module.css b/app/components/Header.module.css index 915c9e0..c8a47c4 100644 --- a/app/components/Header.module.css +++ b/app/components/Header.module.css @@ -5,4 +5,9 @@ padding: 20px min(4vh, 2vw); text-align: center; text-decoration: none; + @media only screen and (max-height: 767px) { + font-size: xx-large; + padding-bottom: 10px; + padding-top: 10px; + } } \ No newline at end of file diff --git a/app/components/OpenLayers.tsx b/app/components/OpenLayers.tsx index 526d32c..693deb8 100644 --- a/app/components/OpenLayers.tsx +++ b/app/components/OpenLayers.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef} from 'react'; import {Feature, Map, View} from 'ol'; import {Coordinate} from 'ol/coordinate'; -import {Extent} from "ol/extent"; +import {Extent} from 'ol/extent'; import {Point, Polygon} from 'ol/geom'; import TileLayer from 'ol/layer/Tile'; import VectorLayer from "ol/layer/Vector"; @@ -10,6 +10,7 @@ import {fromLonLat} from 'ol/proj'; import {OSM} from 'ol/source'; import VectorSource from 'ol/source/Vector'; import {Fill, Stroke, Style} from 'ol/style'; +import CircleStyle from 'ol/style/Circle'; import 'ol/ol.css'; import styles from './OpenLayers.module.css'; @@ -36,7 +37,9 @@ function createPointVectorLayer(name: string, source: VectorSource(undefined); @@ -70,7 +73,7 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { let pointVectorLayer = createPointVectorLayer(pointVectorLayerName, pointVectorSource); let polygonVectorLayer = createPolygonVectorLayer(polygonVectorLayerName, polygonVectorSource); - const style = new Style({ + const normalPolygonStyle = new Style({ fill: new Fill({ color: 'rgba(0, 128, 255, 0.4)', }), @@ -80,6 +83,29 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { }), }); + const highlightPolygonStyle = new Style({ + fill: new Fill({ + color: 'rgba(255, 0, 0, 0.4)', + }), + stroke: new Stroke({ + color: 'red', + width: 2, + }), + }); + + const highlightPointStyle = new Style({ + image: new CircleStyle({ + fill: new Fill({ + color: 'rgba(255, 0, 0, 0.4)' + }), + stroke: new Stroke({ + color: 'red', + width: 1 + }), + radius: 5 + }) + }); + function updateAgriCrops () { const map = mapRef.current; if (!map || agriCrops.length === 0) return; @@ -93,7 +119,7 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { lonLatCoordinates.push(fromLonLat(coordinate)); }); const polygonFeature = new Feature({ geometry: new Polygon([lonLatCoordinates]) }); - polygonFeature.setStyle(style); + polygonFeature.setStyle(agriCrop.id === selectedAgriCrop?.id ? highlightPolygonStyle : normalPolygonStyle); features.push(polygonFeature); }); polygonVectorSource.clear(); @@ -105,7 +131,9 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { } polygonVectorLayer = createPolygonVectorLayer(polygonVectorLayerName, polygonVectorSource); map.addLayer(polygonVectorLayer); - fitMap(map.getView(), polygonVectorSource.getExtent()); + if (!selectedAgriCrop && !selectedSensor) { + fitMap(map.getView(), polygonVectorSource.getExtent()); + } } function updateSensors() { @@ -116,7 +144,11 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { return !selectedGroup || sensor.customGroup === selectedGroup.groupId; }); _sensors.forEach((sensor: Sensor) => { - features.push(new Feature({ geometry: new Point(fromLonLat(sensor.coordinates)) })); + const pointFeature = new Feature({ geometry: new Point(fromLonLat(sensor.coordinates)) }); + if (sensor.id === selectedSensor?.id) { + pointFeature.setStyle(highlightPointStyle); + } + features.push(pointFeature); }); pointVectorSource.clear(); pointVectorSource.addFeatures(features); @@ -148,13 +180,13 @@ function OpenLayers({ agriCrops, id, selectedGroup, sensors }: Props) { if (mapRef.current && agriCrops.length > 0) { updateAgriCrops(); } - }, [agriCrops, selectedGroup]); + }, [agriCrops, selectedGroup, selectedAgriCrop]); useEffect(() => { if (mapRef.current && sensors.length > 0) { updateSensors(); } - }, [sensors, selectedGroup]); + }, [sensors, selectedGroup, selectedSensor]); return
; } diff --git a/app/components/SingleSelectionGroupContents.tsx b/app/components/SingleSelectionGroupContents.tsx new file mode 100644 index 0000000..9155523 --- /dev/null +++ b/app/components/SingleSelectionGroupContents.tsx @@ -0,0 +1,57 @@ +import React, {ChangeEventHandler, MouseEventHandler} from 'react'; +import {Button, Form} from 'react-bootstrap'; +import {NavLink} from 'react-router'; + +import AgriCrop from '../models/AgriCrop'; +import Sensor from '../models/Sensor'; + +import styles from './SingleSelectionGroups.module.css'; + +interface Props { + agriCrops: AgriCrop[], + handleChange: ChangeEventHandler, + handleReset: MouseEventHandler, + selectedAgriCrop: AgriCrop | undefined, + selectedSensor: Sensor | undefined, + sensors: Sensor[] +} + +function SingleSelectionGroupContents({ agriCrops, handleChange, handleReset, selectedAgriCrop, selectedSensor, sensors }: Props) { + return <> +

Übersicht der Gruppeninhalte:

+
+ {sensors.map((sensor) => ( + + ))} + {agriCrops.map((agriCrops) => ( + + ))} + Zurück + + + ; +} + +export default SingleSelectionGroupContents; \ No newline at end of file diff --git a/app/components/SingleSelectionGroups.tsx b/app/components/SingleSelectionGroups.tsx index 0edd457..6435600 100644 --- a/app/components/SingleSelectionGroups.tsx +++ b/app/components/SingleSelectionGroups.tsx @@ -1,14 +1,15 @@ import styles from './SingleSelectionGroups.module.css'; -import React from 'react'; +import React, {ChangeEventHandler, MouseEventHandler} from 'react'; import {Button, Form} from 'react-bootstrap'; +import {NavLink} from 'react-router'; import TenantGroup from '../models/TenantGroup'; interface Props { groups: TenantGroup[], - handleChange: any, - handleReset: any, + handleChange: ChangeEventHandler, + handleReset: MouseEventHandler, selectedGroup: TenantGroup|undefined } @@ -31,9 +32,13 @@ function SingleSelectionGroups({ handleChange, handleReset, groups, selectedGrou onChange={handleChange} /> ))} - + + Weiter + ); diff --git a/app/models/Sensor.tsx b/app/models/Sensor.tsx index e27eeb3..4786991 100644 --- a/app/models/Sensor.tsx +++ b/app/models/Sensor.tsx @@ -3,6 +3,7 @@ import {Coordinate} from 'ol/coordinate'; export default interface Sensor { id: string, type: string, + name: string, customGroup: string, coordinates: Coordinate } diff --git a/app/models/SensorResponse.tsx b/app/models/SensorResponse.tsx index 2bfd5f9..a7f76e5 100644 --- a/app/models/SensorResponse.tsx +++ b/app/models/SensorResponse.tsx @@ -4,5 +4,6 @@ export default interface SensorResponse { id: string, type: string, customGroup: string, - location: SingleLocationResponse | null + location: SingleLocationResponse | null, + name: string } diff --git a/app/routes.ts b/app/routes.ts index c0ddb40..caf5f5d 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -2,5 +2,6 @@ import {index, route, RouteConfig} from '@react-router/dev/routes'; export default [ index('./routes/Home.tsx'), - route('impressum', './routes/Impressum.tsx') + route('impressum', './routes/Impressum.tsx'), + route('group/:groupName', './routes/Group.tsx') ] satisfies RouteConfig; \ No newline at end of file diff --git a/app/routes/Group.tsx b/app/routes/Group.tsx new file mode 100644 index 0000000..097a475 --- /dev/null +++ b/app/routes/Group.tsx @@ -0,0 +1,163 @@ +import React, {ChangeEvent, useEffect, useState} from 'react'; + +import OpenLayers from '../components/OpenLayers'; +import SingleSelectionGroupContents from '../components/SingleSelectionGroupContents'; +import AgriCrop from '../models/AgriCrop'; +import AgriCropResponse from '../models/AgriCropResponse'; +import Sensor from '../models/Sensor'; +import SensorResponse from '../models/SensorResponse'; +import TenantGroup from '../models/TenantGroup'; +import TenantGroupsResponse from '../models/TenantGroupsResponse'; +import { + getAgriCropPolygonByGroupId, + getAgvolutionSensorsLocationsByGroupId, + getSentekSensorsLocationsByGroupId +} from '../services/fiwareService'; +import {getTenantGroups} from '../services/tenantApiService'; + +import styles from './Home.module.css'; + +interface Props { + params: { + groupName: string + } +} + +function Group({ params }: Props) { + + const groupName = params.groupName; + + const [agriCropsLoaded, setAgriCropsLoaded] = useState(false); + const [agvolutionSensorsLoaded, setAgvolutionSensorsLoaded] = useState(false); + const [sentekSensorsLoaded, setSentekSensorsLoaded] = useState(false); + + const [selectedAgriCrop, setSelectedAgriCrop] = useState(); + const [selectedGroup, setSelectedGroup] = useState(); + const [selectedSensor, setSelectedSensor] = useState(undefined); + + const [agriCrops, setAgriCrops] = useState([]); + const [sensors, setSensors] = useState([]); + + const handleChange = (event: ChangeEvent) => { + const id = event.target.value; + const selectedAgriCrop = agriCrops.find((agriCrop) => agriCrop.id === id); + const selectedSensor = sensors.find((sensor) => sensor.id === id); + setSelectedAgriCrop(selectedAgriCrop); + setSelectedSensor(selectedSensor); + }; + const handleReset = () => { + setSelectedAgriCrop(undefined); + setSelectedSensor(undefined); + }; + + useEffect(() => { + getTenantGroups() + .then((response) => { + const data: TenantGroupsResponse = response.data; + const groups = data.groups; + const selectedGroup = groups.find((_group) => _group.name === groupName); + if (selectedGroup) { + setSelectedGroup(selectedGroup); + } else { + alert(`Die Gruppe ${groupName} konnte nicht gefunden werden.`); + } + }) + .catch((error) => { + console.debug(error); + }); + }, []); + + useEffect(() => { + if (selectedGroup) { + getAgriCropPolygonByGroupId(selectedGroup.groupId) + .then((response) => { + const _agriCrops = response.data; + if (Array.isArray(_agriCrops)) { + const newAgriCrops: AgriCrop[] = []; + _agriCrops.map((_agriCrop: AgriCropResponse) => { + newAgriCrops.push({ + id: _agriCrop.id, + customGroup: _agriCrop.customGroup, + coordinates: _agriCrop.location.coordinates + }); + }); + setAgriCrops(newAgriCrops); + setAgriCropsLoaded(true); + } + }) + .catch((error) => { + console.debug(error); + }); + getAgvolutionSensorsLocationsByGroupId(selectedGroup.groupId) + .then((response) => { + handleSensorsResponse(response.data); + setAgvolutionSensorsLoaded(true); + }) + .catch((error) => { + console.debug(error); + }); + + getSentekSensorsLocationsByGroupId(selectedGroup.groupId) + .then((response) => { + handleSensorsResponse(response.data); + setSentekSensorsLoaded(true); + }) + .catch((error) => { + console.debug(error); + }); + + function handleSensorsResponse(_sensors: SensorResponse[]) { + if (Array.isArray(_sensors)) { + const newSensors: Sensor[] = []; + _sensors.map((_sensor: SensorResponse) => { + if (_sensor.location !== null) { + newSensors.push({ + id: _sensor.id, + type: _sensor.type, + name: _sensor.name, + customGroup: _sensor.customGroup, + coordinates: _sensor.location.coordinates + }); + } + }); + setSensors((oldSensors) => { + newSensors.forEach((newSensor) => { + const index: number = oldSensors + .findIndex((oldSensor: Sensor): boolean => oldSensor?.id === newSensor.id && oldSensor.type === newSensor.type); + if (index >= 0) { + oldSensors.splice(index, 1); + } + }); + return [...oldSensors, ...newSensors] + }); + } + } + } + + }, [ selectedGroup ]); + + const openLayers = agriCropsLoaded && agvolutionSensorsLoaded && sentekSensorsLoaded + ? + :

Loading...

; + + return ( +
+
+ +
+
{openLayers}
+
+ ); +} + +export default Group; \ No newline at end of file diff --git a/app/routes/Home.module.css b/app/routes/Home.module.css index b277f64..8a730f9 100644 --- a/app/routes/Home.module.css +++ b/app/routes/Home.module.css @@ -1,16 +1,26 @@ .container { display: flex; - @media only screen and (max-width: 767px) { + overflow: hidden; + @media only screen and (max-aspect-ratio: 1/1) { flex-wrap: wrap; } height: 100%; .filter, .map { + height: 100%; width: 50%; - @media only screen and (max-width: 767px) { + @media only screen and (max-aspect-ratio: 1/1) { + height: 50%; width: 100%; } } .filter { + overflow-y: auto; + @media only screen and (max-aspect-ratio: 1/1) { + margin-bottom: 20px; + } + @media only screen and (min-aspect-ratio: 1/1) { + margin-right: 10px; + } padding-right: 10px; } } \ No newline at end of file diff --git a/app/routes/Home.tsx b/app/routes/Home.tsx index c31aaf8..1542cd8 100644 --- a/app/routes/Home.tsx +++ b/app/routes/Home.tsx @@ -52,6 +52,9 @@ function Home() { setAgriCrops(newAgriCrops); setAgriCropsLoaded(true); } + }) + .catch((error) => { + console.debug(error); }); function handleSensorsResponse(_sensors: SensorResponse[]) { @@ -62,6 +65,7 @@ function Home() { newSensors.push({ id: _sensor.id, type: _sensor.type, + name: _sensor.name, customGroup: _sensor.customGroup, coordinates: _sensor.location.coordinates }); diff --git a/app/services/fiwareService.tsx b/app/services/fiwareService.tsx index 83cb538..95ff736 100644 --- a/app/services/fiwareService.tsx +++ b/app/services/fiwareService.tsx @@ -17,19 +17,37 @@ function getRequestFromFiwareServer(params = {}, headers = {}) { export function getAgvolutionSensorsLocations() { return getRequestFromFiwareServer({ type: 'AgvolutionSensor', - attrs: 'customGroup,location', + attrs: 'customGroup,location,name', options: 'keyValues' }); } +export function getAgvolutionSensorsLocationsByGroupId(groupId: string) { + return getRequestFromFiwareServer({ + type: 'AgvolutionSensor', + attrs: 'customGroup,location,name', + options: 'keyValues', + q: `customGroup==${groupId}` + }); +} + export function getSentekSensorsLocations() { return getRequestFromFiwareServer({ type: 'SentekSensor', - attrs: 'customGroup,location', + attrs: 'customGroup,location,name', options: 'keyValues' }); } +export function getSentekSensorsLocationsByGroupId(groupId: string) { + return getRequestFromFiwareServer({ + type: 'SentekSensor', + attrs: 'customGroup,location,name', + options: 'keyValues', + q: `customGroup==${groupId}` + }); +} + export function getAgriCropPolygon() { return getRequestFromFiwareServer({ type: 'AgriCrop', @@ -37,3 +55,12 @@ export function getAgriCropPolygon() { options: 'keyValues' }); } + +export function getAgriCropPolygonByGroupId(groupId: string) { + return getRequestFromFiwareServer({ + type: 'AgriCrop', + attrs: 'customGroup,location', + options: 'keyValues', + q: `customGroup==${groupId}` + }); +} \ No newline at end of file