diff --git a/src/frontend/src/api/CreateProjectService.ts b/src/frontend/src/api/CreateProjectService.ts index 3a8d302de9..b54f2203ca 100755 --- a/src/frontend/src/api/CreateProjectService.ts +++ b/src/frontend/src/api/CreateProjectService.ts @@ -352,6 +352,14 @@ const TaskSplittingPreviewService: Function = ( dispatch(CreateProjectActions.SetIsTasksGenerated({ key: 'task_splitting_algorithm', value: true })); dispatch(CreateProjectActions.GetTaskSplittingPreview(resp)); } catch (error) { + dispatch( + CommonActions.SetSnackBar({ + open: true, + message: 'Task generation failed. Please try again', + variant: 'error', + duration: 2000, + }), + ); dispatch(CreateProjectActions.GetTaskSplittingPreviewLoading(false)); } finally { dispatch(CreateProjectActions.GetTaskSplittingPreviewLoading(false)); diff --git a/src/frontend/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js b/src/frontend/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js index 546bd7fddf..e3cb4c26d7 100644 --- a/src/frontend/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js +++ b/src/frontend/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js @@ -74,8 +74,10 @@ const VectorLayer = ({ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857', }); + const geometry = vectorLayer.getSource().getFeatures()?.[0].getGeometry(); + const area = formatArea(geometry); - onModify(geoJSONString); + onModify(geoJSONString, area); }); map.addInteraction(modify); map.addInteraction(select); @@ -191,22 +193,26 @@ const VectorLayer = ({ useEffect(() => { if (!vectorLayer || !style.visibleOnMap || setStyle) return; - vectorLayer.setStyle((feature, resolution) => [ - new Style({ - image: new CircleStyle({ - radius: 5, - fill: new Fill({ - color: 'orange', - }), - }), - geometry: function (feature) { - // return the coordinates of the first ring of the polygon - const coordinates = feature.getGeometry().getCoordinates()[0]; - return new MultiPoint(coordinates); - }, - }), - getStyles({ style, feature, resolution }), - ]); + vectorLayer.setStyle((feature, resolution) => { + return onModify + ? [ + new Style({ + image: new CircleStyle({ + radius: 5, + fill: new Fill({ + color: 'orange', + }), + }), + geometry: function (feature) { + // return the coordinates of the first ring of the polygon + const coordinates = feature.getGeometry().getCoordinates()[0]; + return new MultiPoint(coordinates); + }, + }), + getStyles({ style, feature, resolution }), + ] + : [getStyles({ style, feature, resolution })]; + }); }, [vectorLayer, style, setStyle]); useEffect(() => { @@ -254,7 +260,6 @@ const VectorLayer = ({ }); function pointerMovefn(event) { vectorLayer.getFeatures(event.pixel).then((features) => { - console.log(selection, 'selection'); if (!features.length) { selection = {}; hoverEffect(undefined, vectorLayer); diff --git a/src/frontend/src/components/common/Button.tsx b/src/frontend/src/components/common/Button.tsx index 49168f80e7..a5f69f748d 100644 --- a/src/frontend/src/components/common/Button.tsx +++ b/src/frontend/src/components/common/Button.tsx @@ -12,6 +12,7 @@ interface IButton { icon?: React.ReactNode; isLoading?: boolean; disabled?: boolean; + loadingText?: string; } const btnStyle = (btnType, className) => { @@ -24,13 +25,25 @@ const btnStyle = (btnType, className) => { case 'other': return `fmtm-py-1 fmtm-px-5 fmtm-bg-red-500 fmtm-text-white fmtm-rounded-lg hover:fmtm-bg-red-600`; case 'disabled': - return `fmtm-py-1 fmtm-px-4 fmtm-text-white fmtm-rounded-lg fmtm-bg-gray-400 fmtm-cursor-not-allowed`; + return `fmtm-py-1 fmtm-px-4 fmtm-text-white fmtm-rounded-lg fmtm-bg-gray-400 fmtm-cursor-not-allowed ${className}`; default: return 'fmtm-primary'; } }; -const Button = ({ btnText, btnType, type, onClick, disabled, className, count, dataTip, icon, isLoading }: IButton) => ( +const Button = ({ + btnText, + btnType, + type, + onClick, + disabled, + className, + count, + dataTip, + icon, + isLoading, + loadingText, +}: IButton) => ( <div className="fmtm-w-fit"> <button type={type === 'submit' ? 'submit' : 'button'} @@ -44,7 +57,7 @@ const Button = ({ btnText, btnType, type, onClick, disabled, className, count, d > {isLoading ? ( <> - {type === 'submit' ? 'Submitting...' : 'Loading...'} + {type === 'submit' ? 'Submitting...' : loadingText ? loadingText : 'Loading...'} <Loader2 className="fmtm-mr-2 fmtm-h-6 fmtm-w-6 fmtm-animate-spin" /> </> ) : ( diff --git a/src/frontend/src/components/createnewproject/DataExtract.tsx b/src/frontend/src/components/createnewproject/DataExtract.tsx index 42e57c2a85..fd2dd1553c 100644 --- a/src/frontend/src/components/createnewproject/DataExtract.tsx +++ b/src/frontend/src/components/createnewproject/DataExtract.tsx @@ -1,6 +1,6 @@ import axios from 'axios'; import { geojson as fgbGeojson } from 'flatgeobuf'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import Button from '../../components/common/Button'; import { useDispatch } from 'react-redux'; import { CommonActions } from '../../store/slices/CommonSlice'; @@ -25,27 +25,66 @@ const osmFeatureTypeOptions = [ { name: 'osm_feature_type', value: 'polygon', label: 'Polygon' }, ]; +enum FeatureTypeName { + point_centroid = 'Point/Centroid', + line = 'Line', + polygon = 'Polygon', +} + const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygonUpload, setCustomPolygonUpload }) => { const dispatch = useDispatch(); const navigate = useNavigate(); + const [extractWays, setExtractWays] = useState(''); + const [featureType, setFeatureType] = useState(''); const projectDetails: any = useAppSelector((state) => state.createproject.projectDetails); const drawnGeojson = useAppSelector((state) => state.createproject.drawnGeojson); const dataExtractGeojson = useAppSelector((state) => state.createproject.dataExtractGeojson); + const isFgbFetching = useAppSelector((state) => state.createproject.isFgbFetching); + + const submission = () => { + if (featureType !== formValues?.dataExtractFeatureType && formValues.dataExtractWays === 'osm_data_extract') { + dispatch( + CommonActions.SetSnackBar({ + open: true, + message: `Please generate data extract for ${FeatureTypeName[featureType]}`, + variant: 'warning', + duration: 2000, + }), + ); + return; + } - const submission = async () => { dispatch(CreateProjectActions.SetIndividualProjectDetailsData(formValues)); dispatch(CommonActions.SetCurrentStepFormStep({ flag: flag, step: 5 })); // First go to next page, to not block UX navigate('/split-tasks'); + }; + + const resetFile = (setDataExtractToState) => { + setDataExtractToState(null); + }; + + const { + handleSubmit, + handleCustomChange, + values: formValues, + errors, + }: any = useForm(projectDetails, submission, DataExtractValidation); + // Generate OSM data extract + const generateDataExtract = async () => { // Get OSM data extract if required - if (formValues.dataExtractWays === 'osm_data_extract') { + if (extractWays === 'osm_data_extract') { + // Remove current data extract + dispatch(CreateProjectActions.setDataExtractGeojson(null)); + // Create a file object from the project area Blob const projectAreaBlob = new Blob([JSON.stringify(drawnGeojson)], { type: 'application/json' }); const drawnGeojsonFile = new File([projectAreaBlob], 'outline.json', { type: 'application/json' }); + dispatch(CreateProjectActions.SetFgbFetchingStatus(true)); // Create form and POST endpoint const dataExtractRequestFormData = new FormData(); dataExtractRequestFormData.append('geojson_file', drawnGeojsonFile); @@ -56,9 +95,16 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo ); const fgbUrl = response.data.url; - // Append url to project data + // Append url to project data & remove custom files dispatch( - CreateProjectActions.SetIndividualProjectDetailsData({ ...projectDetails, data_extract_type: fgbUrl }), + CreateProjectActions.SetIndividualProjectDetailsData({ + ...formValues, + data_extract_type: fgbUrl, + dataExtractWays: extractWays, + dataExtractFeatureType: featureType, + customLineUpload: null, + customPolygonUpload: null, + }), ); // Extract fgb and set geojson to map @@ -67,21 +113,43 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo const uint8ArrayData = new Uint8Array(binaryData); // Deserialize the binary data const geojsonExtract = await fgbGeojson.deserialize(uint8ArrayData); + dispatch(CreateProjectActions.SetFgbFetchingStatus(false)); await dispatch(CreateProjectActions.setDataExtractGeojson(geojsonExtract)); } catch (error) { + dispatch( + CommonActions.SetSnackBar({ + open: true, + message: 'Error to generate FGB file.', + variant: 'error', + duration: 2000, + }), + ); + dispatch(CreateProjectActions.SetFgbFetchingStatus(false)); // TODO add error message for user console.error('Error getting data extract:', error); } } }; - const { - handleSubmit, - handleCustomChange, - values: formValues, - errors, - }: any = useForm(projectDetails, submission, DataExtractValidation); + + useEffect(() => { + if (formValues?.dataExtractWays) { + setExtractWays(formValues?.dataExtractWays); + } + if (formValues?.dataExtractFeatureType) { + setFeatureType(formValues?.dataExtractFeatureType); + } + }, [formValues?.dataExtractWays, formValues?.dataExtractFeatureType]); const toggleStep = (step, url) => { + if (url === '/select-form') { + dispatch( + CreateProjectActions.SetIndividualProjectDetailsData({ + ...formValues, + dataExtractWays: extractWays, + dataExtractFeatureType: featureType, + }), + ); + } dispatch(CommonActions.SetCurrentStepFormStep({ flag: flag, step: step })); navigate(url); }; @@ -137,10 +205,6 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo await dispatch(CreateProjectActions.setDataExtractGeojson(extractFeatCol)); }; - const resetFile = (setDataExtractToState) => { - setDataExtractToState(null); - }; - useEffect(() => { dispatch(FormCategoryService(`${import.meta.env.VITE_API_URL}/central/list-forms`)); }, []); @@ -179,31 +243,59 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo value={formValues.dataExtractWays} onChangeData={(value) => { handleCustomChange('dataExtractWays', value); + setExtractWays(value); }} errorMsg={errors.dataExtractWays} /> - {formValues.dataExtractWays === 'osm_data_extract' && ( + {extractWays === 'osm_data_extract' && ( <div className="fmtm-mt-6"> <RadioButton topic="Select OSM feature type" options={osmFeatureTypeOptions} direction="column" - value={formValues.dataExtractFeatureType} + value={featureType} onChangeData={(value) => { - handleCustomChange('dataExtractFeatureType', value); + setFeatureType(value); }} errorMsg={errors.dataExtractFeatureType} /> </div> )} - {formValues.dataExtractWays === 'custom_data_extract' && ( + {extractWays === 'osm_data_extract' && featureType && ( + <Button + btnText="Generate Data Extract" + btnType="primary" + onClick={() => { + resetFile(setCustomPolygonUpload); + resetFile(setCustomLineUpload); + generateDataExtract(); + }} + className="fmtm-mt-6" + isLoading={isFgbFetching} + loadingText="Data extracting..." + disabled={ + featureType === formValues?.dataExtractFeatureType && + dataExtractGeojson && + !customPolygonUpload && + !customLineUpload + ? true + : false + } + /> + )} + {extractWays === 'custom_data_extract' && ( <> <FileInputComponent onChange={(e) => { changeFileHandler(e, setCustomPolygonUpload); handleCustomChange('customPolygonUpload', e.target.files[0]); + handleCustomChange('dataExtractFeatureType', ''); + setFeatureType(''); + }} + onResetFile={() => { + resetFile(setCustomPolygonUpload); + handleCustomChange('customPolygonUpload', null); }} - onResetFile={() => resetFile(setCustomPolygonUpload)} customFile={customPolygonUpload} btnText="Upload Polygons" accept=".geojson,.json,.fgb" @@ -214,8 +306,12 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo onChange={(e) => { changeFileHandler(e, setCustomLineUpload); handleCustomChange('customLineUpload', e.target.files[0]); + handleCustomChange('dataExtractFeatureType', null); + }} + onResetFile={() => { + resetFile(setCustomLineUpload); + handleCustomChange('customLineUpload', null); }} - onResetFile={() => resetFile(setCustomLineUpload)} customFile={customLineUpload} btnText="Upload Lines" accept=".geojson,.json,.fgb" @@ -233,7 +329,20 @@ const DataExtract = ({ flag, customLineUpload, setCustomLineUpload, customPolygo onClick={() => toggleStep(3, '/select-form')} className="fmtm-font-bold" /> - <Button btnText="NEXT" btnType="primary" type="submit" className="fmtm-font-bold" /> + <Button + btnText="NEXT" + btnType="primary" + type="submit" + className="fmtm-font-bold" + dataTip={`${!dataExtractGeojson ? 'Please Generate Data Extract First.' : ''}`} + disabled={ + !dataExtractGeojson || + (extractWays === 'osm_data_extract' && !formValues?.dataExtractFeatureType) || + isFgbFetching + ? true + : false + } + /> </div> </form> <div className="fmtm-w-full lg:fmtm-w-[60%] fmtm-flex fmtm-flex-col fmtm-gap-6 fmtm-bg-gray-300 fmtm-h-[60vh] lg:fmtm-h-full"> diff --git a/src/frontend/src/components/createnewproject/SplitTasks.tsx b/src/frontend/src/components/createnewproject/SplitTasks.tsx index 22234d128d..55e92b5ff6 100644 --- a/src/frontend/src/components/createnewproject/SplitTasks.tsx +++ b/src/frontend/src/components/createnewproject/SplitTasks.tsx @@ -61,6 +61,7 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customLineUpload, custo (state) => state.createproject.taskSplittingGeojsonLoading, ); const isTasksGenerated = CoreModules.useAppSelector((state) => state.createproject.isTasksGenerated); + const isFgbFetching = CoreModules.useAppSelector((state) => state.createproject.isFgbFetching); const toggleStep = (step, url) => { dispatch(CommonActions.SetCurrentStepFormStep({ flag: flag, step: step })); @@ -130,7 +131,6 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customLineUpload, custo } else { projectData = { ...projectData, task_split_dimension: projectDetails.dimension }; } - console.log(projectData, 'projectData'); dispatch( CreateProjectService( `${import.meta.env.VITE_API_URL}/projects/create_project`, @@ -379,8 +379,9 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customLineUpload, custo className="" icon={<AssetModules.SettingsIcon className="fmtm-text-white" />} disabled={ - splitTasksSelection === task_split_type['task_splitting_algorithm'] && - !formValues?.average_buildings_per_task + (splitTasksSelection === task_split_type['task_splitting_algorithm'] && + !formValues?.average_buildings_per_task) || + isFgbFetching ? true : false } @@ -426,6 +427,11 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customLineUpload, custo splittedGeojson={dividedTaskGeojson} uploadedOrDrawnGeojsonFile={drawnGeojson} buildingExtractedGeojson={dataExtractGeojson} + onModify={(geojson) => { + handleCustomChange('drawnGeojson', geojson); + dispatch(CreateProjectActions.SetDividedTaskGeojson(JSON.parse(geojson))); + setGeojsonFile(null); + }} /> </div> {generateProjectLog ? ( diff --git a/src/frontend/src/components/createnewproject/UploadArea.tsx b/src/frontend/src/components/createnewproject/UploadArea.tsx index 01f3c93281..2bcba1ce65 100644 --- a/src/frontend/src/components/createnewproject/UploadArea.tsx +++ b/src/frontend/src/components/createnewproject/UploadArea.tsx @@ -31,7 +31,7 @@ const uploadAreaOptions = [ }, ]; -const UploadArea = ({ flag, geojsonFile, setGeojsonFile }) => { +const UploadArea = ({ flag, geojsonFile, setGeojsonFile, setCustomLineUpload, setCustomPolygonUpload }) => { const dispatch = useDispatch(); const navigate = useNavigate(); // const [uploadAreaFile, setUploadAreaFile] = useState(null); @@ -264,6 +264,9 @@ const UploadArea = ({ flag, geojsonFile, setGeojsonFile }) => { handleCustomChange('drawnGeojson', geojson); dispatch(CreateProjectActions.SetDrawnGeojson(JSON.parse(geojson))); dispatch(CreateProjectActions.SetTotalAreaSelection(area)); + dispatch(CreateProjectActions.ClearProjectStepState()); + setCustomLineUpload(null); + setCustomPolygonUpload(null); setGeojsonFile(null); }} /> diff --git a/src/frontend/src/store/slices/CreateProjectSlice.ts b/src/frontend/src/store/slices/CreateProjectSlice.ts index 7f9cc75856..5de2a1cea2 100755 --- a/src/frontend/src/store/slices/CreateProjectSlice.ts +++ b/src/frontend/src/store/slices/CreateProjectSlice.ts @@ -48,6 +48,7 @@ export const initialState: CreateProjectStateTypes = { isUnsavedChanges: false, canSwitchCreateProjectSteps: false, isTasksGenerated: { divide_on_square: false, task_splitting_algorithm: false }, + isFgbFetching: false, }; const CreateProject = createSlice({ @@ -215,6 +216,15 @@ const CreateProject = createSlice({ [action.payload.key]: action.payload.value, }; }, + SetFgbFetchingStatus(state, action) { + state.isFgbFetching = action.payload; + }, + ClearProjectStepState(state) { + state.dividedTaskGeojson = null; + state.splitTasksSelection = null; + state.dataExtractGeojson = null; + state.projectDetails = { ...state.projectDetails, customLineUpload: null, customPolygonUpload: null }; + }, }, }); diff --git a/src/frontend/src/store/types/ICreateProject.ts b/src/frontend/src/store/types/ICreateProject.ts index 900dff66de..bd7fd27e08 100644 --- a/src/frontend/src/store/types/ICreateProject.ts +++ b/src/frontend/src/store/types/ICreateProject.ts @@ -34,6 +34,7 @@ export type CreateProjectStateTypes = { isUnsavedChanges: boolean; canSwitchCreateProjectSteps: boolean; isTasksGenerated: {}; + isFgbFetching: boolean; }; export type ValidateCustomFormResponse = { detail: { message: string; possible_reason: string }; diff --git a/src/frontend/src/views/CreateNewProject.tsx b/src/frontend/src/views/CreateNewProject.tsx index 14f6ac96bb..271d9b4dc1 100644 --- a/src/frontend/src/views/CreateNewProject.tsx +++ b/src/frontend/src/views/CreateNewProject.tsx @@ -61,7 +61,15 @@ const CreateNewProject = () => { case '/create-project': return <ProjectDetailsForm flag="create_project" />; case '/upload-area': - return <UploadArea flag="create_project" geojsonFile={geojsonFile} setGeojsonFile={setGeojsonFile} />; + return ( + <UploadArea + flag="create_project" + geojsonFile={geojsonFile} + setGeojsonFile={setGeojsonFile} + setCustomLineUpload={setCustomLineUpload} + setCustomPolygonUpload={setCustomPolygonUpload} + /> + ); case '/select-form': return ( <SelectForm diff --git a/src/frontend/src/views/NewDefineAreaMap.tsx b/src/frontend/src/views/NewDefineAreaMap.tsx index 72d0b8d82e..15364b012e 100644 --- a/src/frontend/src/views/NewDefineAreaMap.tsx +++ b/src/frontend/src/views/NewDefineAreaMap.tsx @@ -7,11 +7,12 @@ import { GeoJSONFeatureTypes } from '../store/types/ICreateProject'; type NewDefineAreaMapProps = { drawToggle?: boolean; - splittedGeojson: GeoJSONFeatureTypes; + splittedGeojson: GeoJSONFeatureTypes | null; uploadedOrDrawnGeojsonFile: GeoJSONFeatureTypes; buildingExtractedGeojson?: GeoJSONFeatureTypes; lineExtractedGeojson?: GeoJSONFeatureTypes; - onDraw?: () => void; + onDraw?: (geojson: any, area: number) => void; + onModify?: (geojson: any, area?: number) => void; }; const NewDefineAreaMap = ({ drawToggle, @@ -51,7 +52,7 @@ const NewDefineAreaMap = ({ constrainResolution: true, duration: 500, }} - zoomToLayer + onModify={onModify} /> )} {isDrawOrGeojsonFile && !splittedGeojson && (