diff --git a/.github/workflows/backend_build.yml b/.github/workflows/backend_build.yml index 09f3a993..bb697925 100644 --- a/.github/workflows/backend_build.yml +++ b/.github/workflows/backend_build.yml @@ -92,10 +92,10 @@ jobs: - name: Create env run: | cd backend/ - mv sample_env .env - export DATABASE_URL=postgis://admin:password@localhost:5432/ai - export RAMP_HOME="/home/runner/work/fAIr/fAIr" - export TRAINING_WORKSPACE="/home/runner/work/fAIr/fAIr/backend/training" + + export DATABASE_URL=$DATABASE_URL + export RAMP_HOME=$RAMP_HOME + export TRAINING_WORKSPACE=$TRAINING_WORKSPACE - name: Run celery worker run: | @@ -120,6 +120,9 @@ jobs: - name: Run migrations env: TESTING_TOKEN: ${{ secrets.TESTING_TOKEN }} + OSM_CLIENT_ID: ${{ secrets.OSM_CLIENT_ID}} + OSM_CLIENT_SECRET: ${{ secrets.OSM_CLIENT_SECRET}} + OSM_SECRET_KEY: ${{ secrets.OSM_SECRET_KEY}} run: | cd backend/ python manage.py makemigrations diff --git a/.github/workflows/frontend_build.yml b/.github/workflows/frontend_build.yml index 23b091c1..c9b1ec9a 100644 --- a/.github/workflows/frontend_build.yml +++ b/.github/workflows/frontend_build.yml @@ -2,43 +2,41 @@ name: Frontend Build on: push: - branches: [ master ] + branches: [master] paths: - - 'frontend/**' - - '.github/workflows/frontend_build.yml' + - "frontend/**" + - ".github/workflows/frontend_build.yml" pull_request: - branches: [ master ] + branches: [master] paths: - - 'frontend/**' - - '.github/workflows/frontend_build.yml' + - "frontend/**" + - ".github/workflows/frontend_build.yml" jobs: Build_On_Ubuntu: - runs-on: ubuntu-latest env: CI: false strategy: matrix: - node-version: [ 16.14.2, 16, 18, 20 ] + node-version: [16, 18, 20] steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: | - cd frontend/ - npm install --legacy-peer-deps - - - name: Build - run: | - cd frontend/ - npm run build - + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: | + cd frontend/ + npm install --legacy-peer-deps + + - name: Build + run: | + cd frontend/ + npm run build diff --git a/backend/tests/test_endpoints.py b/backend/tests/test_endpoints.py index a20a3f84..af60fa1a 100644 --- a/backend/tests/test_endpoints.py +++ b/backend/tests/test_endpoints.py @@ -132,6 +132,8 @@ def test_create_training(self): "model": self.model.id, } res = self.client.post( + f"{API_BASE}/training/", + json.dumps(training_payload), f"{API_BASE}/training/", json.dumps(training_payload), headers=json_type_header, @@ -148,6 +150,8 @@ def test_create_training(self): "model": self.model.id, } res = self.client.post( + f"{API_BASE}/training/", + json.dumps(training_payload), f"{API_BASE}/training/", json.dumps(training_payload), headers=json_type_header, @@ -168,6 +172,7 @@ def test_create_training(self): json.dumps(training_payload), headers=json_type_header, ) + print(res.json()) self.assertEqual(res.status_code, status.HTTP_201_CREATED) # create another training for the same model diff --git a/frontend/package.json b/frontend/package.json index 4604544b..b0ab2404 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,9 +2,6 @@ "name": "fair", "version": "0.1.0", "private": true, - "engines": { - "node": "16.14.2" - }, "dependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", @@ -18,6 +15,8 @@ "@mui/material": "^5.6.1", "@mui/styles": "^5.12.0", "@mui/x-data-grid": "^5.17.12", + "@mui/x-tree-view": "^7.11.0", + "@terraformer/wkt": "^2.2.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", @@ -62,5 +61,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "ajv": "^7.2.4" } } diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/FilesTree.js b/frontend/src/components/Layout/AIModels/AIModelEditor/FilesTree.js index 48729c50..9ff8f7cf 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/FilesTree.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/FilesTree.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import TreeView from "@mui/lab/TreeView"; +import { SimpleTreeView } from "@mui/x-tree-view"; import TreeItem from "@mui/lab/TreeItem"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; @@ -73,7 +73,7 @@ const FilesTree = (props) => { )} - } defaultExpandIcon={} @@ -126,7 +126,7 @@ const FilesTree = (props) => { {/* */} - + ); }; diff --git a/frontend/src/components/Layout/Home/Home.js b/frontend/src/components/Layout/Home/Home.js index 4211cf6d..8dd5072c 100644 --- a/frontend/src/components/Layout/Home/Home.js +++ b/frontend/src/components/Layout/Home/Home.js @@ -26,23 +26,13 @@ const GetStarted = () => { variant="body1" style={{ color: "#3D3D3D", fontSize: "18px", marginBottom: "50px" }} > - fAIr is an open AI-assisted mapping service developed by the - Humanitarian OpenStreetMap Team (HOT) that aims to improve the - efficiency and accuracy of mapping efforts for humanitarian purposes. - The service uses AI models, specifically computer vision techniques, to - detect objects such as buildings, roads, waterways, and trees from - satellite and UAV imagery. The name fAIr is derived from the following - terms: + fAIr performs mapping in the same way as human mappers using HOT's Tasking Manager. It looks at UAV imagery and produces map data that can be added to OpenStreetMap (OSM). Tests show a 100% speedup compared to manual mapping. It uses Artificial Intelligence (AI) to accomplish this.

- + fAIr is developed by the Humanitarian OpenStreetMap Team (HOT) and all the software is free and open source. +
+
+ Before fAIr is used, it needs to be fine-tuned by training on high quality map data for a small representative part of the geographical region where it is to be used.
({ backgroundColor: theme.palette.background.paper, })); @@ -40,11 +41,30 @@ const ListItemWithWiderSecondaryAction = withStyles({ })(ListItem); const PER_PAGE = 5; + +const postAoi = async (polygon, dataset, accessToken) => { + console.log("Posting AOI"); + console.log(dataset); + const headers = { + "Content-Type": "application/json", + "access-token": accessToken, + }; + const data = { + geom: `SRID=4326;${polygon}`, + dataset, + }; + const response = await axios.post("/aoi/", data, { headers }); + console.log(response.data); + return response.data; +}; + const AOI = (props) => { const [dense, setDense] = useState(true); const count = Math.ceil(props.mapLayers.length / PER_PAGE); let [page, setPage] = useState(1); const [openSnack, setOpenSnack] = useState(false); + const [fileError, setFileError] = useState(null); + const [geoJsonFile, setGeoJsonFile] = useState(null); let _DATA = usePagination( props.mapLayers.filter((e) => e.type === "aoi"), PER_PAGE @@ -53,7 +73,7 @@ const AOI = (props) => { setPage(p); _DATA.jump(p); }; - // console.log("_DATA", _DATA); + useEffect(() => { return () => {}; }, [props]); @@ -70,16 +90,12 @@ const AOI = (props) => { }); if (res.error) { - // setMapError(res.error.response.statusText); console.log(res.error.response.statusText); } else { - // success full fetch - return res.data; } } catch (e) { console.log("isError", e); - } finally { } }; const { mutate: mutateFetch, data: fetchResult } = @@ -106,11 +122,74 @@ const AOI = (props) => { } } catch (e) { console.log("isError", e); - } finally { } }; const { mutate: mutateDeleteAOI } = useMutation(DeleteAOI); + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + if (file) { + const fileName = file.name.toLowerCase(); + if (!fileName.endsWith(".geojson")) { + setFileError("Invalid file format. Please upload a .geojson file."); + return; + } + const reader = new FileReader(); + reader.onload = async (e) => { + try { + const geoJson = JSON.parse(e.target.result); + let geometry; + + if (geoJson.type === "FeatureCollection") { + // if (geoJson.features.length > 1) { + // setFileError( + // "Feature collection contains multiple features. Only uploaded first one" + // ); + // } + // TODO : for featurecollection loop through the features and add AOI one by one + const feature = geoJson.features[0]; + if ( + feature.geometry.type !== "Polygon" && + feature.geometry.type !== "MultiPolygon" + ) { + setFileError("GeoJSON must contain a Polygon or MultiPolygon."); + return; + } + geometry = feature.geometry; + } else if (geoJson.type === "Feature") { + if ( + geoJson.geometry.type !== "Polygon" && + geoJson.geometry.type !== "MultiPolygon" + ) { + setFileError( + "Feature geometry type must be Polygon or MultiPolygon." + ); + return; + } + geometry = geoJson.geometry; + } else if ( + geoJson.type === "Polygon" || + geoJson.type === "MultiPolygon" + ) { + geometry = geoJson; + } else { + setFileError("Invalid GeoJSON format."); + return; + } + + const wkt = Terraformer.geojsonToWKT(geometry); + await postAoi(wkt, props.datasetId, accessToken); + setFileError(null); + setGeoJsonFile(null); + } catch (error) { + console.error(error); + setFileError("Error processing GeoJSON file."); + } + }; + reader.readAsText(file); + } + }; + return ( <> @@ -119,6 +198,28 @@ const AOI = (props) => { Training Areas{` (${props.mapLayers.length})`} + + + {fileError && ( + setFileError(null)}> + {fileError} + + )} {props.mapLayers && props.mapLayers.length > PER_PAGE && ( { "" )} - {/* add here a container to get the AOI status from DB */} {layer.aoiId && ( )} @@ -167,40 +267,6 @@ const AOI = (props) => { } /> - {/* - - */} - {/* - { - // mutateFetch(layer.aoiId); - // console.log("Open in Editor") - window.open( - `https://rapideditor.org/rapid#background=${ - props.oamImagery - ? "custom:" + props.oamImagery.url - : "Bing" - }&datasets=fbRoads,msBuildings&disable_features=boundaries&map=16.00/17.9253/120.4841&gpx=&gpx=${ - process.env.REACT_APP_API_BASE - }/aoi/gpx/${ - layer.aoiId - }`, - "_blank", - "noreferrer" - ); - }} - > - - RapiD logo - - */} { className="margin1 transparent" onClick={async (e) => { try { - // mutateFetch(layer.aoiId); - console.log("layer", layer); - const Imgurl = new URL( "http://127.0.0.1:8111/imagery" ); @@ -224,10 +287,6 @@ const AOI = (props) => { props.oamImagery.url ); const imgResponse = await fetch(Imgurl); - // bounds._southWest.lng, - // bounds._southWest.lat, - // bounds._northEast.lng, - // bounds._northEast.lat, const loadurl = new URL( "http://127.0.0.1:8111/load_and_zoom" ); @@ -270,8 +329,6 @@ const AOI = (props) => { sx={{ width: 24, height: 24 }} className="margin1 transparent" onClick={(e) => { - // mutateFetch(layer.aoiId); - // console.log("Open in Editor") window.open( `https://www.openstreetmap.org/edit/#background=${ props.oamImagery @@ -285,7 +342,6 @@ const AOI = (props) => { ); }} > - {/* */} OSM logo { className="margin1" onClick={(e) => { mutateFetch(layer.aoiId); - console.log("Call raw data API to fetch OSM data"); }} > - {/* { - - console.log("Remove labels") - }}> - - */} { return accumulator + curValue.lng; }, 0) / layer.latlngs.length; - // [lat, lng] are the centroid of the polygon props.selectAOIHandler([lat, lng], 17); }} > @@ -352,9 +397,6 @@ const AOI = (props) => { sx={{ width: 24, height: 24 }} className="margin-left-13" onClick={(e) => { - // console.log( - // `layer.aoiId ${layer.aoiId} and layer.id ${layer.id}` - // ); mutateDeleteAOI(layer.aoiId, layer.id); }} > @@ -377,7 +419,6 @@ const AOI = (props) => { open={openSnack} autoHideDuration={5000} onClose={() => { - console.log("openSnack", openSnack); setOpenSnack(false); }} message={ @@ -395,7 +436,6 @@ const AOI = (props) => { } - // action={action} color="red" anchorOrigin={{ vertical: "bottom", horizontal: "right" }} /> diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js index 10bfb8f4..10560775 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js @@ -117,6 +117,7 @@ function DatasetEditor() { mapLayers={mapLayers.filter((i) => i.type === "aoi")} selectAOIHandler={selectAOIHandler} deleteAOIButton={deleteAOIButton} + datasetId={dataset.id} >