From 3183a761c588f42de5ba9905f3c3f5c7f48ed2f6 Mon Sep 17 00:00:00 2001 From: Sujit <90745363+suzit-10@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:13:58 +0545 Subject: [PATCH] feat: Show warning if altitude exceeds country's max limit (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add drone altitude regulations model * feat: Add 'get all' and 'get one' endpoints for drone altitudes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * reac: Rename DbDroneAltitude class to DbDroneFlightHeight and update table name * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: add `OSM_NOMINATIM_URL` on viteconfig and env.example * feat(project-creation): get project country by project centroid using `OSM Nominatim API` find centroid with the help of `truf` Integrated Nominatim API to fetch the country based on the centroid coordinates * feat: add className optional prop to InfoMessage component for dynamic styling * feat(create-project): Show warning if altitude exceeds country’s max limit * feat(individual-project): disable popup only if the user role regulator and has no other roles * feat(create-project): update fields description * feat: remove nominatim api from .env and vite config and save as a constant variable since it is constant on all environment * feat: update .env.example * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Pradip-p Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sujit --- .../CreateprojectLayout/index.tsx | 36 +++++++++++++++++-- .../FormContents/KeyParameters/index.tsx | 35 +++++++++++++++--- .../IndividualProject/MapSection/index.tsx | 7 +++- .../common/FormUI/InfoMessage/index.tsx | 5 +-- src/frontend/src/constants/createProject.tsx | 15 ++++++++ src/frontend/src/services/common.ts | 9 +++++ src/frontend/src/services/createproject.ts | 3 ++ src/frontend/src/store/slices/common.ts | 2 ++ 8 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx index 76694d4e..3d0f704f 100644 --- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx +++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; import { useTypedSelector, useTypedDispatch } from '@Store/hooks'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { FieldValues, useForm } from 'react-hook-form'; import { BasicInformationForm, @@ -12,6 +12,7 @@ import { import { UseFormPropsType } from '@Components/common/FormUI/types'; import { FlexRow } from '@Components/common/Layouts'; import { Button } from '@Components/RadixComponents/Button'; +import centroid from '@turf/centroid'; import { resetUploadedAndDrawnAreas, setCreateProjectState, @@ -26,7 +27,9 @@ import { convertGeojsonToFile } from '@Utils/convertLayerUtils'; import prepareFormData from '@Utils/prepareFormData'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; import { getFrontOverlap, getSideOverlap, gsdToAltitude } from '@Utils/index'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { getCountry } from '@Services/common'; +import { setCommonState } from '@Store/actions/common'; /** * This function looks up the provided map of components to find and return @@ -65,6 +68,7 @@ const getActiveStepForm = (activeStep: number, formProps: UseFormPropsType) => { const CreateprojectLayout = () => { const dispatch = useTypedDispatch(); const navigate = useNavigate(); + const [projectCentroid, setProjectCentroid] = useState(null); const activeStep = useTypedSelector(state => state.createproject.activeStep); const splitGeojson = useTypedSelector( @@ -191,6 +195,24 @@ const CreateprojectLayout = () => { dispatch(setCreateProjectState({ activeStep: activeStep - 1 })); }; + const { isFetching: isFetchingCountry } = useQuery({ + queryFn: () => + getCountry({ + lon: projectCentroid?.[0] || 0, + lat: projectCentroid?.[1] || 0, + format: 'json', + }), + queryKey: ['country', projectCentroid?.[0], projectCentroid?.[1]], + enabled: !!projectCentroid, + select(data) { + dispatch( + setCommonState({ + projectCountry: data?.data?.address?.country || null, + }), + ); + }, + }); + const onSubmit = (data: any) => { if (activeStep === 2) { if ( @@ -207,6 +229,8 @@ const CreateprojectLayout = () => { toast.error('Please upload or draw and save No Fly zone area'); return; } + const newCentroid = centroid(data.outline)?.geometry?.coordinates; + setProjectCentroid(newCentroid); } if (activeStep === 3) { @@ -297,6 +321,7 @@ const CreateprojectLayout = () => { splitGeojson: null, uploadedProjectArea: null, uploadedNoFlyZone: null, + projectCountry: null, }), ); }; @@ -336,7 +361,12 @@ const CreateprojectLayout = () => { className="!naxatw-bg-red !naxatw-text-white" rightIcon="chevron_right" withLoader - isLoading={isLoading || isCreatingProject || !capturedProjectMap} + isLoading={ + isLoading || + isCreatingProject || + !capturedProjectMap || + isFetchingCountry + } disabled={isLoading || isCreatingProject || !capturedProjectMap} > {activeStep === 5 ? 'Save' : 'Next'} diff --git a/src/frontend/src/components/CreateProject/FormContents/KeyParameters/index.tsx b/src/frontend/src/components/CreateProject/FormContents/KeyParameters/index.tsx index ff288cda..a09a5ccc 100644 --- a/src/frontend/src/components/CreateProject/FormContents/KeyParameters/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/KeyParameters/index.tsx @@ -5,6 +5,8 @@ import ErrorMessage from '@Components/common/FormUI/ErrorMessage'; import { UseFormPropsType } from '@Components/common/FormUI/types'; import { setCreateProjectState } from '@Store/actions/createproject'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; +import { useQuery } from '@tanstack/react-query'; +import { getDroneAltitude } from '@Services/createproject'; // import { terrainOptions } from '@Constants/createProject'; import { FlexRow } from '@Components/common/Layouts'; import Switch from '@Components/RadixComponents/Switch'; @@ -25,6 +27,7 @@ import { } from '@Utils/index'; import SwitchTab from '@Components/common/SwitchTab'; import { Controller } from 'react-hook-form'; + import OutputOptions from './OutputOptions'; const KeyParameters = ({ formProps }: { formProps: UseFormPropsType }) => { @@ -51,6 +54,14 @@ const KeyParameters = ({ formProps }: { formProps: UseFormPropsType }) => { const imageMergeType = useTypedSelector( state => state.createproject.imageMergeType, ); + const projectCountry = useTypedSelector(state => state.common.projectCountry); + + const { data: droneAltitude } = useQuery({ + queryKey: ['drone-altitude', projectCountry], + queryFn: () => getDroneAltitude(projectCountry || ''), + select: data => data.data, + enabled: !!projectCountry, + }); // get altitude const agl = @@ -115,7 +126,9 @@ const KeyParameters = ({ formProps }: { formProps: UseFormPropsType }) => { ) : ( <> )} - + {errors?.gsd_cm_px?.message && ( + + )} ) : ( @@ -146,11 +159,25 @@ const KeyParameters = ({ formProps }: { formProps: UseFormPropsType }) => { <> )} - + {errors?.altitude_from_ground?.message && ( + + )} )} + + {droneAltitude?.country && + droneAltitude?.max_altitude_ft && + (altitudeInputValue > droneAltitude?.max_altitude_m || + gsdToAltitude(Number(gsdInputValue)) > + droneAltitude?.max_altitude_m) && ( + + )} + diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 82dabb8d..60776a4b 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -265,7 +265,12 @@ const MapSection = ({ projectData }: { projectData: Record }) => { return ( feature?.source?.includes('tasks-layer') && - !userDetails?.role?.includes('REGULATOR') // Don't show popup if user role is regulator + !( + ( + userDetails?.role?.length === 1 && + userDetails?.role?.includes('REGULATOR') + ) // Don't show popup if user role is regulator any and no other roles + ) ); }} fetchPopupData={(properties: Record) => { diff --git a/src/frontend/src/components/common/FormUI/InfoMessage/index.tsx b/src/frontend/src/components/common/FormUI/InfoMessage/index.tsx index cb34b6f4..8b5f7c54 100644 --- a/src/frontend/src/components/common/FormUI/InfoMessage/index.tsx +++ b/src/frontend/src/components/common/FormUI/InfoMessage/index.tsx @@ -1,12 +1,13 @@ interface IInfoMessageProp { message: string; + className?: string; } -const InfoMessage = ({ message }: IInfoMessageProp) => { +const InfoMessage = ({ message, className }: IInfoMessageProp) => { return ( {message} diff --git a/src/frontend/src/constants/createProject.tsx b/src/frontend/src/constants/createProject.tsx index a11010ad..bd65eaea 100644 --- a/src/frontend/src/constants/createProject.tsx +++ b/src/frontend/src/constants/createProject.tsx @@ -215,6 +215,21 @@ export const contributionsInfo = [ key: 'Instructions for Drone Operators', description: 'Detailed instructions or parameters for the drone operation.', }, + { + key: 'Deadline for Submission', + description: 'Date for specifying when the project should be submitted.', + }, + { + key: 'Does this project require approval from the local regulatory committee?', + description: + 'Indicate if the project requires approval from the local regulatory committee before proceeding.', + }, + { + key: 'Local regulation committee Email Address', + description: + 'The email addresses of local regulatory committee members. If one of them approves or rejects the project, it will be processed accordingly. This is required for areas where drone flights are restricted and require permission', + }, + { key: 'Approval for task lock', description: diff --git a/src/frontend/src/services/common.ts b/src/frontend/src/services/common.ts index 5faed1b3..36261e29 100644 --- a/src/frontend/src/services/common.ts +++ b/src/frontend/src/services/common.ts @@ -1,6 +1,9 @@ import { UserProfileDetailsType } from '@Components/GoogleAuth/types'; +import axios from 'axios'; import { api, authenticated } from '.'; +const OSM_NOMINATIM_URL = 'https://nominatim.openstreetmap.org'; + export const signInUser = (data: any) => api.post('/users/login/', data); export const signInGoogle = () => api.get('/users/google-login'); @@ -27,3 +30,9 @@ export const patchUserProfile = ({ userId, data }: Record) => authenticated(api).patch(`/users/${userId}/profile`, data, { headers: { 'Content-Type': 'application/json' }, }); + +export const getCountry = (params: { + lat: number; + lon: number; + format: string; +}) => axios.get(`${OSM_NOMINATIM_URL}/reverse`, { params }); diff --git a/src/frontend/src/services/createproject.ts b/src/frontend/src/services/createproject.ts index 50a559bd..f3118ed1 100644 --- a/src/frontend/src/services/createproject.ts +++ b/src/frontend/src/services/createproject.ts @@ -36,3 +36,6 @@ export const regulatorComment = (payload: Record) => { }, ); }; + +export const getDroneAltitude = (country: string) => + authenticated(api).get(`/drones/drone-altitude/${country}/`); diff --git a/src/frontend/src/store/slices/common.ts b/src/frontend/src/store/slices/common.ts index fa667d5b..5f8f1e8d 100644 --- a/src/frontend/src/store/slices/common.ts +++ b/src/frontend/src/store/slices/common.ts @@ -22,6 +22,7 @@ export interface CommonState { isCertifiedDroneUser: 'yes' | 'no'; projectSearchKey: string; selectedDocumentDetails: documentDetailType | null; + projectCountry: string | null; } const initialState: CommonState = { @@ -35,6 +36,7 @@ const initialState: CommonState = { isCertifiedDroneUser: 'no', projectSearchKey: '', selectedDocumentDetails: null, + projectCountry: null, }; const setCommonState: CaseReducer<