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/frontend/package.json b/frontend/package.json index 4604544b..bcb8a0b7 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,7 @@ "@mui/material": "^5.6.1", "@mui/styles": "^5.12.0", "@mui/x-data-grid": "^5.17.12", + "@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 +60,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "ajv": "^7.2.4" } } 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} >