├── .eslintrc ├── .github └── workflows │ └── checks.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── lambda ├── README.md ├── app.py ├── main.tf ├── output.tf └── variables.tf ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.png ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── components │ ├── ApiService.js │ ├── ClassObj.js │ ├── Classes.js │ ├── CreateProjectModal.js │ ├── Decode.js │ ├── DecodeAutomatic.js │ ├── DecodeItems.js │ ├── DecodePointPromt.js │ ├── EncodeCanvas.js │ ├── EncodeImport.js │ ├── EncodeItem.js │ ├── EncodeItems.js │ ├── Header.js │ ├── Item.js │ ├── Items.js │ ├── ItemsDataActions.js │ ├── MenuExpData.js │ ├── MenuTemplate.js │ ├── Modal.js │ ├── Projects.js │ ├── Sidebar.js │ ├── SpinerLoader.js │ └── map │ │ ├── MagicWand.js │ │ ├── ProjectLayer.js │ │ ├── index.js │ │ └── layers.js ├── config │ └── index.js ├── contexts │ └── MainContext.js ├── index.css ├── index.js ├── media │ ├── icons │ │ ├── background.svg │ │ └── foreground.svg │ ├── layout │ │ ├── ds-logo-pos.svg │ │ └── logo.svg │ └── meta │ │ └── favicon.png ├── reducers │ └── index.js ├── static │ └── projects.json ├── store │ └── indexedDB.js └── utils │ ├── calculation.js │ ├── canvas.js │ ├── convert.js │ ├── notifications.js │ ├── requests.js │ ├── tests │ ├── featureCollection.test.js │ ├── fixtures │ │ └── index.js │ ├── transformation.test.js │ └── util.test.js │ ├── transformation.js │ └── utils.js └── tailwind.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "react-app/jest" 5 | ], 6 | "plugins": ["unused-imports"], 7 | 8 | "rules": { 9 | "comma-dangle": ["error", "always-multiline"], 10 | "unused-imports/no-unused-imports": "error" 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | # This workflow performs basic checks: 2 | # 3 | # 1. run a preparation step to install and cache node modules 4 | # 2. once prep succeeds, lint and test run in parallel 5 | # 6 | # The checks only run on non-draft Pull Requests. They don't run on the main 7 | # branch prior to deploy. It's recommended to use branch protection to avoid 8 | # pushes straight to 'main'. 9 | 10 | name: Checks 11 | 12 | on: 13 | pull_request: 14 | types: 15 | - opened 16 | - synchronize 17 | - reopened 18 | - ready_for_review 19 | 20 | env: 21 | NODE: 16 22 | 23 | jobs: 24 | prep: 25 | if: github.event.pull_request.draft == false 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Cancel Previous Runs 30 | uses: styfle/cancel-workflow-action@0.8.0 31 | with: 32 | access_token: ${{ github.token }} 33 | 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Use Node.js ${{ env.NODE }} 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: ${{ env.NODE }} 41 | 42 | - name: Cache node_modules 43 | uses: actions/cache@v2 44 | id: cache-node-modules 45 | with: 46 | path: node_modules 47 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 48 | 49 | - name: Install 50 | run: yarn install 51 | 52 | lint: 53 | needs: prep 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v2 59 | 60 | - name: Use Node.js ${{ env.NODE }} 61 | uses: actions/setup-node@v1 62 | with: 63 | node-version: ${{ env.NODE }} 64 | 65 | - name: Cache node_modules 66 | uses: actions/cache@v2 67 | id: cache-node-modules 68 | with: 69 | path: node_modules 70 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 71 | 72 | - name: Install 73 | run: yarn install 74 | 75 | - name: Lint 76 | run: yarn lint 77 | 78 | test: 79 | needs: prep 80 | runs-on: ubuntu-latest 81 | 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@v2 85 | 86 | - name: Use Node.js ${{ env.NODE }} 87 | uses: actions/setup-node@v1 88 | with: 89 | node-version: ${{ env.NODE }} 90 | 91 | - name: Cache node_modules 92 | uses: actions/cache@v2 93 | id: cache-node-modules 94 | with: 95 | path: node_modules 96 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 97 | 98 | - name: Install 99 | run: yarn install 100 | 101 | - name: Test 102 | run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | yarn.lock 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | package-lock.json 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Local .terraform directories 27 | **/.terraform/* 28 | 29 | # .tfstate files 30 | *.tfstate 31 | *.tfstate.* 32 | *.hcl 33 | # Crash log files 34 | crash.log 35 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 36 | # .tfvars files are managed as part of configuration and so should be included in 37 | # version control. 38 | # 39 | # example.tfvars 40 | 41 | # Ignore override files as they are usually used to override resources locally and so 42 | # are not checked in 43 | override.tf 44 | override.tf.json 45 | *_override.tf 46 | *_override.tf.json 47 | *.zip 48 | # Include override files you do wish to add to version control using negated pattern 49 | # 50 | # !example_override.tf 51 | 52 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 53 | # example: *tfplan* 54 | .terraform 55 | *.tfstate.backup 56 | *.pem 57 | secrets.tfvars 58 | yarn.lock 59 | /.contentlayer -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "printWidth": 80 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Development Seed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ds-annotate 2 | 3 | Magic wand and Segment Anything Model (SAM2) annotation tool for machine learning training data. 4 | 5 | ![370998787-c8532ae9-d073-4381-b466-adfca76d7d381-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/fb5b951e-643a-43b6-b80c-a5e545b6b9db) 6 | 7 | 8 | 9 | **This application consumes a service API from [samgeo-service](https://github.com/GeoCompas/samgeo-service).** 10 | 11 | 15 | 16 | 17 | ## Installation and Usage 18 | The steps below will walk you through setting up your own instance of the project. 19 | 20 | ### Install Project Dependencies 21 | To set up the development environment for this website, you'll need to install the following on your system: 22 | 23 | - [Node](http://nodejs.org/) v16 (To manage multiple node versions we recommend [nvm](https://github.com/creationix/nvm)) 24 | - [Yarn](https://yarnpkg.com/) Package manager 25 | 26 | ### Install Application Dependencies 27 | 28 | If you use [`nvm`](https://github.com/creationix/nvm), activate the desired Node version: `v16.14.2` 29 | 30 | ``` 31 | nvm install 32 | ``` 33 | 34 | Install Node modules: 35 | 36 | ``` 37 | yarn install 38 | ``` 39 | 40 | ## Usage 41 | 42 | Ds-Anotate tool can be used the [online](http://devseed.com/ds-annotate) version with you custom values: 43 | 44 | 45 | - **classes**: List of classes for labeling. 46 | - **name**: Project name 47 | - **imagery_type**: Type of aerial Imagery: `tms` or `wms`. 48 | - **imagery_url**: Url for imagery service. 49 | - **project_bbox**: Location where you need to focus on. 50 | 51 | 52 | *Example:* 53 | 54 | ``` 55 | classes=farm,00FFFF|tree,FF00FF 56 | project=Farms_mapping 57 | imagery_type=tms 58 | imagery_url=https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false 59 | project_bbox=-90.319317,38.482965,-90.247220,38.507418 60 | ``` 61 | 62 | [http://devseed.com/ds-annotate?classes=farm,00FFFF|tree,FF00FF&project=Farms-mapping&imagery_type=tms&imagery_url=https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false&project_bbox=-90.319317,38.482965,-90.247220,38.507418](http://devseed.com/ds-annotate?classes=farm,00FFFF|tree,FF00FF&project=Farms-mapping&imagery_type=tms&imagery_url=https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false&project_bbox=-90.319317,38.482965,-90.247220,38.507418) 63 | ### Starting the app 64 | 65 | ``` 66 | yarn serve 67 | ``` 68 | Compiles the sass files, javascript, and launches the server making the site available at `http://localhost:9000/` 69 | The system will watch files and execute tasks whenever one of them changes. 70 | The site will automatically refresh since it is bundled with livereload. 71 | 72 | # Deployment 73 | To prepare the app for deployment run: 74 | 75 | ``` 76 | yarn install 77 | yarn start 78 | ``` 79 | -------------------------------------------------------------------------------- /lambda/README.md: -------------------------------------------------------------------------------- 1 | # Lambda function 2 | This is a Terramore template to create a lambda function, the lambda function goal is to save the draws into s3 bucket and make available through presigned URL in order to load in JOSM the geojson files. 3 | 4 | 5 | ### Execute 6 | 7 | ```sh 8 | cd lambda/ 9 | terraform init 10 | terraform plan 11 | terraform apply 12 | # terraform destroy 13 | ``` 14 | 15 | The output will be the API-Getway url 16 | -------------------------------------------------------------------------------- /lambda/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from datetime import datetime 4 | import boto3 5 | 6 | def lambda_handler(event, context): 7 | now = datetime.now() 8 | date_time = now.strftime("%Y-%m-%d_%H-%M-%S") 9 | s3 = boto3.client("s3") 10 | bucket = os.environ["S3_BUCKET"] 11 | obj = json.loads(event["body"]) 12 | key = obj.get("filename", f"{date_time}") 13 | s3.put_object( 14 | Body=(bytes(json.dumps(obj["data"]).encode("UTF-8"))), Bucket=bucket, Key=key 15 | ) 16 | # Generate the presigned URL 17 | presigned_url = s3.generate_presigned_url( 18 | "get_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=3600 19 | ) 20 | return { 21 | "statusCode": 200, 22 | "headers": {"Content-Type": "application/json"}, 23 | "body": json.dumps({"url": presigned_url}), 24 | } 25 | -------------------------------------------------------------------------------- /lambda/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.aws_region 3 | } 4 | provider "archive" {} 5 | data "archive_file" "zip" { 6 | type = "zip" 7 | source_file = "app.py" 8 | output_path = "app.zip" 9 | } 10 | 11 | ##### Create policy documents for assume role and s3 permissions 12 | data "aws_iam_policy_document" "lambda_assume_role" { 13 | statement { 14 | actions = ["sts:AssumeRole"] 15 | principals { 16 | type = "Service" 17 | identifiers = ["lambda.amazonaws.com"] 18 | } 19 | } 20 | } 21 | 22 | data "aws_iam_policy_document" "lambda_s3_access" { 23 | statement { 24 | actions = [ 25 | "s3:PutObject", 26 | "s3:PutObjectAcl", 27 | "s3:GetObject", 28 | "s3:GetObjectAcl", 29 | ] 30 | resources = [ 31 | "arn:aws:s3:::${var.s3_bucket}/*" 32 | ] 33 | } 34 | } 35 | 36 | ##### Create an IAM policy 37 | resource "aws_iam_policy" "lambda_s3_iam_policy" { 38 | name = "ds_annotate_lambda_s3_permissions" 39 | description = "Contains S3 put permission for lambda" 40 | policy = data.aws_iam_policy_document.lambda_s3_access.json 41 | } 42 | 43 | ##### Create a role 44 | resource "aws_iam_role" "lambda_role" { 45 | name = "ds_annotate_lambda_role" 46 | assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json 47 | } 48 | 49 | ##### Attach policy to role 50 | resource "aws_iam_role_policy_attachment" "lambda_s3" { 51 | role = aws_iam_role.lambda_role.name 52 | policy_arn = aws_iam_policy.lambda_s3_iam_policy.arn 53 | } 54 | 55 | resource "aws_lambda_function" "ds_annotate_lambda" { 56 | function_name = "ds_annotate_lambda" 57 | filename = data.archive_file.zip.output_path 58 | source_code_hash = data.archive_file.zip.output_base64sha256 59 | role = aws_iam_role.lambda_role.arn 60 | handler = "app.lambda_handler" 61 | runtime = "python3.9" 62 | memory_size = 256 63 | environment { 64 | variables = { 65 | S3_BUCKET = var.s3_bucket 66 | } 67 | } 68 | } 69 | 70 | resource "aws_cloudwatch_log_group" "ds_annotate_lambda_cloudwatch" { 71 | name = "/aws/lambda/${aws_lambda_function.ds_annotate_lambda.function_name}" 72 | retention_in_days = 1 73 | } 74 | 75 | resource "aws_iam_role_policy_attachment" "lambda_policy" { 76 | role = aws_iam_role.lambda_role.name 77 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 78 | } 79 | 80 | 81 | ##### Api Gateway 82 | resource "aws_apigatewayv2_api" "lambda" { 83 | name = "ds_annotate_serverless_lambda_gw" 84 | protocol_type = "HTTP" 85 | cors_configuration { 86 | allow_headers = ["*"] 87 | allow_methods = ["POST"] 88 | allow_origins = ["http://devseed.com", "https://devseed.com", "http://localhost:3000"] 89 | } 90 | } 91 | 92 | resource "aws_apigatewayv2_stage" "lambda" { 93 | api_id = aws_apigatewayv2_api.lambda.id 94 | name = "ds_annotate" 95 | auto_deploy = true 96 | access_log_settings { 97 | destination_arn = aws_cloudwatch_log_group.api_gw.arn 98 | format = jsonencode({ 99 | requestId = "$context.requestId" 100 | sourceIp = "$context.identity.sourceIp" 101 | requestTime = "$context.requestTime" 102 | protocol = "$context.protocol" 103 | httpMethod = "$context.httpMethod" 104 | resourcePath = "$context.resourcePath" 105 | routeKey = "$context.routeKey" 106 | status = "$context.status" 107 | responseLength = "$context.responseLength" 108 | integrationErrorMessage = "$context.integrationErrorMessage" 109 | } 110 | ) 111 | } 112 | } 113 | 114 | resource "aws_apigatewayv2_integration" "ds_aws_apigateway_integration" { 115 | api_id = aws_apigatewayv2_api.lambda.id 116 | integration_uri = aws_lambda_function.ds_annotate_lambda.invoke_arn 117 | integration_type = "AWS_PROXY" 118 | integration_method = "POST" 119 | } 120 | 121 | resource "aws_apigatewayv2_route" "ds_aws_apigateway_route" { 122 | api_id = aws_apigatewayv2_api.lambda.id 123 | route_key = "POST /" 124 | target = "integrations/${aws_apigatewayv2_integration.ds_aws_apigateway_integration.id}" 125 | } 126 | 127 | resource "aws_cloudwatch_log_group" "api_gw" { 128 | name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}" 129 | retention_in_days = 30 130 | } 131 | 132 | resource "aws_lambda_permission" "api_gw" { 133 | statement_id = "AllowExecutionFromAPIGateway" 134 | action = "lambda:InvokeFunction" 135 | function_name = aws_lambda_function.ds_annotate_lambda.function_name 136 | principal = "apigateway.amazonaws.com" 137 | source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*" 138 | } 139 | -------------------------------------------------------------------------------- /lambda/output.tf: -------------------------------------------------------------------------------- 1 | 2 | output "ds_annotate_lambda" { 3 | description = "Name of the Lambda function." 4 | value = aws_lambda_function.ds_annotate_lambda.function_name 5 | } 6 | output "base_url" { 7 | description = "Base URL for API Gateway stage." 8 | value = "export API_GATEWAY=${aws_apigatewayv2_stage.lambda.invoke_url}" 9 | } -------------------------------------------------------------------------------- /lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | default = "us-east-1" 3 | } 4 | 5 | variable "s3_bucket" { 6 | default = "ds-annotate" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ds-annotate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "http://devseed.com/ds-annotate/", 6 | "dependencies": { 7 | "@emotion/react": "^11.10.6", 8 | "@testing-library/jest-dom": "^5.16.4", 9 | "@testing-library/react": "^13.3.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@turf/bezier-spline": "^6.5.0", 12 | "@turf/helpers": "^6.5.0", 13 | "@turf/simplify": "^6.5.0", 14 | "@turf/turf": "^6.5.0", 15 | "@types/geojson": "^7946.0.10", 16 | "history": "^5.3.0", 17 | "lodash": "^4.17.21", 18 | "ol": "^6.13.0", 19 | "ol-magic-wand": "^1.0.5", 20 | "prop-types": "^15.8.1", 21 | "react": "^18.2.0", 22 | "react-color": "^2.19.3", 23 | "react-dom": "^18.2.0", 24 | "react-icons": "^4.4.0", 25 | "react-notifications": "^1.7.4", 26 | "react-router-dom": "^6.3.0", 27 | "react-scripts": "5.0.1", 28 | "react-spinners": "^0.13.8", 29 | "web-vitals": "^2.1.4" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "clean": "rimraf build/", 34 | "build": "react-scripts build", 35 | "eject": "react-scripts eject", 36 | "deploy": "yarn clean && PUBLIC_URL=/ds-annotate REACT_APP_ENV=production yarn build && gh-pages -d build", 37 | "deploy_staging": "yarn clean && PUBLIC_URL=/ REACT_APP_ENV=staging yarn build && aws s3 rm s3://ds-annotate-staging/ --recursive && aws s3 sync build/ s3://ds-annotate-staging/", 38 | "lint": "npx eslint --fix . && yarn prettier", 39 | "prettier": "yarn clean && prettier --write 'src/**/*.js'", 40 | "test": "yarn lint && react-scripts test --env=jsdom --watchAll=false --testMatch **/src/**/*.test.js" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "@babel/cli": "^7.13.0", 62 | "@babel/core": "^7.13.1", 63 | "@babel/node": "^7.10.5", 64 | "@babel/plugin-transform-modules-commonjs": "^7.13.0", 65 | "@babel/preset-env": "^7.11.0", 66 | "@babel/preset-react": "^7.12.13", 67 | "@testing-library/react-hooks": "^8.0.1", 68 | "autoprefixer": "^10.4.7", 69 | "babel-jest": "^29.2.1", 70 | "babel-plugin-transform-class-properties": "^6.24.1", 71 | "babel-preset-jest": "^26.6.2", 72 | "cross-env": "^7.0.3", 73 | "eslint": "^8.0.0", 74 | "eslint-config-react-app": "7.0.1", 75 | "eslint-plugin-unused-imports": "^4.1.4", 76 | "gh-pages": "^4.0.0", 77 | "jest": "^26.6.3", 78 | "jest-cli": "^26.6.3", 79 | "jest-watch-typeahead": "0.6.5", 80 | "postcss": "^8.4.14", 81 | "prettier": "^2.6.2", 82 | "rimraf": "^5.0.1", 83 | "tailwindcss": "^3.1.5", 84 | "ts-jest": "^29.0.3" 85 | }, 86 | "jest": { 87 | "coverageReporters": [ 88 | "html" 89 | ], 90 | "transformIgnorePatterns": [ 91 | "node_modules/@uiw/react-md-editor/" 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/ds-annotate/e820a758e44ec6633aaaada690dfc04afe6cdc42/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 28 | DS-Annotate 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/ds-annotate/e820a758e44ec6633aaaada690dfc04afe6cdc42/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/ds-annotate/e820a758e44ec6633aaaada690dfc04afe6cdc42/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Sidebar } from "./components/Sidebar"; 3 | import { MapWrapper } from "./components/map"; 4 | import { Modal } from "./components/Modal"; 5 | import "react-notifications/lib/notifications.css"; 6 | import { NotificationContainer } from "react-notifications"; 7 | import { SpinerLoader } from "./components/SpinerLoader"; 8 | import { ApiService } from "./components/ApiService"; 9 | 10 | function App() { 11 | return ( 12 |
13 | {/* Sidebar */} 14 |
15 | 16 |
17 | 18 | {/* Map Wrapper */} 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | {/* ApiService info*/} 28 | 29 | 30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/components/ApiService.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { getRequest } from "../utils/requests"; 4 | 5 | export const ApiService = () => { 6 | const { 7 | map, 8 | pointsSelector: ps, 9 | activeProject: ap, 10 | activeClass: ac, 11 | dispatchSetItems: dsi, 12 | items, 13 | encodeItems: ei, 14 | dispatchEncodeItems: dei, 15 | activeEncodeImageItem: aei, 16 | dispatchActiveEncodeImageItem: daei, 17 | setSpinnerLoading: ssl, 18 | } = useContext(MainContext); 19 | 20 | const [apiDetails, setApiDetails] = useState({ 21 | device: "", 22 | gpu: { 23 | device_name: "", 24 | num_gpus: 0, 25 | gpu_memory_total: "", 26 | gpu_memory_allocated: "", 27 | gpu_memory_cached: "", 28 | gpu_memory_free: "", 29 | }, 30 | cpu: { 31 | cpu_percent: 0, 32 | cpu_cores: 0, 33 | cpu_logical_cores: 0, 34 | }, 35 | memory: { 36 | total_memory: "", 37 | used_memory: "", 38 | free_memory: "", 39 | memory_percent: 0, 40 | }, 41 | }); 42 | 43 | useEffect(() => { 44 | const fetchApiDetails = async () => { 45 | try { 46 | const result = await getRequest(""); 47 | result && setApiDetails(result); 48 | } catch (err) { 49 | console.error("Error:", err); 50 | } 51 | }; 52 | fetchApiDetails(); 53 | }, [map, ps, ap, ac, dsi, items, ei, dei, aei, daei, ssl]); 54 | 55 | return ( 56 |
57 | 64 | {apiDetails.device === "cuda" ? "GPU Active" : "CPU Mode"} 65 | {apiDetails.device === "cuda" && ( 66 | <> 67 | {` | Dev: ${apiDetails.gpu.device_name}`} 68 | {` | GPUs: ${apiDetails.gpu.num_gpus}`} 69 | {` | Total Mem: ${apiDetails.gpu.gpu_memory_total}`} 70 | {` | Alloc Mem: ${apiDetails.gpu.gpu_memory_allocated}`} 71 | {` | Cached Mem: ${apiDetails.gpu.gpu_memory_cached}`} 72 | {` | Free Mem: ${apiDetails.gpu.gpu_memory_free}`} 73 | 74 | )} 75 | {` | CPU: ${apiDetails.cpu.cpu_percent}%`} 76 | {` | Cores: ${apiDetails.cpu.cpu_cores}`} 77 | {` | Log Cores: ${apiDetails.cpu.cpu_logical_cores}`} 78 | {` | Mem Use: ${apiDetails.memory.memory_percent}%`} 79 | {` | Total: ${apiDetails.memory.total_memory}`} 80 | {` | Used: ${apiDetails.memory.used_memory}`} 81 | {` | Free: ${apiDetails.memory.free_memory}`} 82 | 83 |
84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/ClassObj.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { BsSquareFill } from "react-icons/bs"; 3 | 4 | import { MainContext } from "../contexts/MainContext"; 5 | 6 | export const ClassObj = ({ classProps }) => { 7 | const { activeClass, dispatchSetActiveClass } = useContext(MainContext); 8 | const isActive = activeClass && activeClass.name === classProps.name; 9 | 10 | const setActiveClass = (class_) => { 11 | dispatchSetActiveClass({ 12 | type: "SET_ACTIVE_CLASS", 13 | payload: class_, 14 | }); 15 | }; 16 | 17 | return ( 18 |
  • setActiveClass(classProps)} 23 | > 24 | 25 | 26 | 27 | 28 | {classProps.name} 29 | 30 |
  • 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/Classes.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from "react"; 2 | 3 | import { MainContext } from "../contexts/MainContext"; 4 | import { ClassObj } from "./ClassObj"; 5 | import { getClassLayers } from "../utils/convert"; 6 | import { MenuTemplate } from "./MenuTemplate"; 7 | import { BsListUl } from "react-icons/bs"; 8 | 9 | export const Badge = ({ activeClass }) => { 10 | return ( 11 | 15 | {activeClass ? activeClass.name : ""} 16 | 17 | ); 18 | }; 19 | 20 | export const Classes = () => { 21 | const { activeProject, activeClass } = useContext(MainContext); 22 | const [classes, setClasses] = useState([]); 23 | const [isOpen, setIsOpen] = useState(true); 24 | 25 | useEffect(() => { 26 | const classLayers = getClassLayers(activeProject); 27 | setClasses(classLayers); 28 | setIsOpen(true); 29 | }, [activeProject]); 30 | return ( 31 | } 34 | icon={} 35 | openMenu={isOpen} 36 | setOpenMenu={setIsOpen} 37 | > 38 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/CreateProjectModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { ChromePicker } from "react-color"; 3 | import { FaPalette } from "react-icons/fa"; 4 | 5 | export const CreateProjectModal = ({ isOpen, onClose }) => { 6 | const [formData, setFormData] = useState({ 7 | name: "", 8 | imagery_type: "tms", 9 | imagery_url: "", 10 | project_bbox: "", 11 | }); 12 | 13 | const [classes, setClasses] = useState([{ name: "", color: "#00FFFF" }]); 14 | const [activeColorPicker, setActiveColorPicker] = useState(null); 15 | 16 | const colorPickerRef = useRef(null); 17 | 18 | 19 | useEffect(() => { 20 | const handleClickOutside = (event) => { 21 | if ( 22 | colorPickerRef.current && 23 | !colorPickerRef.current.contains(event.target) 24 | ) { 25 | setActiveColorPicker(null); 26 | } 27 | }; 28 | 29 | document.addEventListener("mousedown", handleClickOutside); 30 | return () => { 31 | document.removeEventListener("mousedown", handleClickOutside); 32 | }; 33 | }, []); 34 | 35 | const handleInputChange = (e) => { 36 | const { name, value } = e.target; 37 | setFormData((prevData) => ({ 38 | ...prevData, 39 | [name]: value, 40 | })); 41 | }; 42 | 43 | const handleClassChange = (index, key, value) => { 44 | const updatedClasses = [...classes]; 45 | updatedClasses[index][key] = value; 46 | setClasses(updatedClasses); 47 | }; 48 | 49 | const addNewClass = () => { 50 | setClasses([...classes, { name: "", color: "#FF00FF" }]); 51 | }; 52 | 53 | const removeClass = (index) => { 54 | const updatedClasses = classes.filter((_, i) => i !== index); 55 | setClasses(updatedClasses); 56 | }; 57 | 58 | const handleSubmit = (e) => { 59 | e.preventDefault(); 60 | 61 | 62 | const classesParam = classes 63 | .filter((cls) => cls.name.trim() !== "") 64 | .map((cls) => `${cls.name.trim()},${cls.color.replace("#", "")}`) 65 | .join("|"); 66 | 67 | 68 | const updatedFormData = { 69 | ...formData, 70 | project_bbox: formData.project_bbox || "-180,-90,180,90", 71 | }; 72 | 73 | 74 | const url = `http://devseed.com/ds-annotate?classes=${classesParam}&project=${encodeURIComponent( 75 | updatedFormData.name 76 | )}&imagery_type=${updatedFormData.imagery_type}&imagery_url=${encodeURIComponent( 77 | updatedFormData.imagery_url 78 | )}&project_bbox=${updatedFormData.project_bbox}`; 79 | 80 | 81 | console.log("Generated URL:", url); 82 | 83 | 84 | window.open(url, "_blank"); 85 | 86 | 87 | setFormData({ 88 | name: "", 89 | imagery_type: "tms", 90 | imagery_url: "", 91 | project_bbox: "", 92 | }); 93 | setClasses([{ name: "", color: "#00FFFF" }]); 94 | onClose(); 95 | }; 96 | 97 | if (!isOpen) return null; 98 | 99 | return ( 100 |
    104 |
    e.stopPropagation()} 107 | > 108 |

    Create New Project

    109 |
    110 | {/* Name Field */} 111 |
    112 | 115 | 124 |
    125 | 126 | {/* Classes Section */} 127 |
    128 | 131 | {classes.map((cls, index) => ( 132 |
    133 | {/* Class Name */} 134 | 138 | handleClassChange(index, "name", e.target.value) 139 | } 140 | placeholder="Class name (e.g., farm)" 141 | className="mt-1 block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" 142 | style={{ 143 | backgroundColor: cls.color, 144 | color: "#fff", 145 | }} 146 | /> 147 | {/* Color Picker Icon */} 148 |
    149 | 160 | {activeColorPicker === index && ( 161 |
    165 | 168 | handleClassChange(index, "color", color.hex) 169 | } 170 | /> 171 |
    172 | )} 173 |
    174 | {/* Remove Class Button */} 175 | {index > 0 && ( 176 | 183 | )} 184 |
    185 | ))} 186 | {/* Add Class Button */} 187 | 194 |
    195 | 196 | {/* Imagery Type Field */} 197 |
    198 | 201 | 210 |
    211 | 212 | {/* Imagery URL Field */} 213 |
    214 | 217 | 225 |
    226 | 227 | {/* Project BBox Field */} 228 |
    229 | 232 | 240 |
    241 | 242 | {/* Modal Buttons */} 243 |
    244 | 251 | 257 |
    258 |
    259 |
    260 |
    261 | ); 262 | }; -------------------------------------------------------------------------------- /src/components/Decode.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { fetchGeoJSONData } from "../utils/requests"; 4 | import { features2olFeatures, setProps2Features } from "../utils/convert"; 5 | import { pointsIsInEncodeBbox } from "../utils/calculation"; 6 | import { storeItems } from "../store/indexedDB"; 7 | import { guid } from "../utils/utils"; 8 | 9 | import { DecodeAutomatic } from "./DecodeAutomatic"; 10 | import { DecodePointPromt } from "./DecodePointPromt"; 11 | export const Decode = () => { 12 | const { 13 | map, 14 | pointsSelector, 15 | dispatchSetPointsSelector, 16 | activeProject, 17 | activeClass, 18 | dispatchSetItems, 19 | items, 20 | activeEncodeImageItem, 21 | setSpinnerLoading, 22 | decoderType, 23 | dispatchDecoderType, 24 | } = useContext(MainContext); 25 | 26 | const [displayPointPromtsMenu, setDisplayPointPromtsMenu] = useState(false); 27 | const [isForegroundPromtPoint, setIsForegroundPromtPoint] = useState(true); 28 | 29 | const decodePrompt = async (requestProps) => { 30 | setSpinnerLoading(true); 31 | try { 32 | // const decodeRespJson = await getDecode(requestProps); 33 | 34 | const resp = await fetchGeoJSONData(requestProps); 35 | 36 | const id = guid(); 37 | const features = setProps2Features( 38 | resp.geojson.features, 39 | activeProject, 40 | activeClass, 41 | id 42 | ); 43 | 44 | const olFeatures = features2olFeatures(features); 45 | // Add items 46 | dispatchSetItems({ 47 | type: "SET_ITEMS", 48 | payload: [...items, ...olFeatures], 49 | }); 50 | 51 | // Set empty the point selectors 52 | dispatchSetPointsSelector({ 53 | type: "SET_EMPTY_POINT", 54 | payload: [], 55 | }); 56 | 57 | // save in iddexedDB 58 | features.forEach((feature) => { 59 | storeItems.addData(feature); 60 | }); 61 | setSpinnerLoading(false); 62 | } catch (error) { 63 | // TODO display error 64 | setSpinnerLoading(false); 65 | } 66 | }; 67 | 68 | const requestAutomatic = async (activeEncodeImageItemProps) => { 69 | const resp = await fetchGeoJSONData(activeEncodeImageItemProps); 70 | const id = guid(); 71 | const features = setProps2Features( 72 | resp.geojson.features, 73 | activeProject, 74 | activeClass, 75 | id 76 | ); 77 | const olFeatures = features2olFeatures(features); 78 | // Add items 79 | dispatchSetItems({ 80 | type: "SET_ITEMS", 81 | payload: [...items, ...olFeatures], 82 | }); 83 | }; 84 | 85 | const buildReqProps = ( 86 | activeEncodeImageItem, 87 | pointsSelector, 88 | decoderType 89 | ) => { 90 | let input_point; 91 | let input_label; 92 | // Get pixels fro each point the is in the map 93 | const listPixels = pointsIsInEncodeBbox( 94 | activeEncodeImageItem, 95 | pointsSelector 96 | ); 97 | 98 | if (decoderType === "single_point") { 99 | input_point = listPixels[0]; 100 | input_label = 1; 101 | } else if ( 102 | decoderType === "multi_point" || 103 | decoderType === "multi_point_split" 104 | ) { 105 | input_point = listPixels; 106 | // TODO ,Fix here for foreground and background labels. 107 | input_label = input_point.map((i) => 1); 108 | } 109 | 110 | // Get propos from encode items 111 | const { image_embeddings, image_shape, crs, bbox, zoom } = 112 | activeEncodeImageItem; 113 | 114 | // Build props to request to the decoder API 115 | const reqProps = { 116 | image_embeddings, 117 | image_shape, 118 | input_label, 119 | crs, 120 | bbox, 121 | zoom, 122 | input_point, 123 | decode_type: decoderType, 124 | }; 125 | 126 | return reqProps; 127 | }; 128 | 129 | // Multipoint is activate when press the request decode buttom 130 | const decodeItems = async (multiDecoderType) => { 131 | if (pointsSelector.length === 0) return; 132 | const reqProps = buildReqProps( 133 | activeEncodeImageItem, 134 | pointsSelector, 135 | multiDecoderType 136 | ); 137 | await decodePrompt(reqProps); 138 | }; 139 | 140 | const setDecodeType = (decodeType) => { 141 | // If the decoder type changes, we need to change the single point to the 142 | // latest multi-point clicked. 143 | dispatchDecoderType({ 144 | type: "SET_DECODER_TYPE", 145 | payload: decodeType, 146 | }); 147 | 148 | if (decodeType === "automatic") { 149 | requestAutomatic(activeEncodeImageItem); 150 | } 151 | 152 | if (decodeType === "single_point") { 153 | setDisplayPointPromtsMenu(!displayPointPromtsMenu); 154 | } 155 | }; 156 | 157 | return ( 158 | <> 159 | 160 | 161 | 162 | 163 | ); 164 | }; 165 | -------------------------------------------------------------------------------- /src/components/DecodeAutomatic.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { NotificationManager } from "react-notifications"; 3 | import { MainContext } from "../contexts/MainContext"; 4 | import { requestSegments } from "../utils/requests"; 5 | import { 6 | features2olFeatures, 7 | setProps2Features, 8 | convertBbox3857to4326, 9 | } from "../utils/convert"; 10 | import { storeItems } from "../store/indexedDB"; 11 | 12 | export const DecodeAutomatic = () => { 13 | const { 14 | activeProject, 15 | activeClass, 16 | dispatchSetItems, 17 | items, 18 | activeEncodeImageItem, 19 | setSpinnerLoading, 20 | decoderType, 21 | dispatchDecoderType, 22 | } = useContext(MainContext); 23 | 24 | const requestAutomatic = async (decodeType) => { 25 | if (!activeEncodeImageItem) { 26 | NotificationManager.warning( 27 | `Select an AOI for making predictions within it.`, 28 | 3000 29 | ); 30 | return; 31 | } 32 | dispatchDecoderType({ 33 | type: "SET_DECODER_TYPE", 34 | payload: decodeType, 35 | }); 36 | 37 | setSpinnerLoading(true); 38 | 39 | const reqProps = { 40 | bbox: convertBbox3857to4326(activeEncodeImageItem.bbox), 41 | crs: "EPSG:4326", 42 | zoom: activeEncodeImageItem.zoom, 43 | id: activeEncodeImageItem.id, 44 | project: activeProject.properties.slug, 45 | return_format: "geojson", 46 | simplify_tolerance: 0.000002, 47 | area_val: 10, 48 | }; 49 | 50 | const resp = await requestSegments(reqProps, "segment_automatic"); 51 | const features = setProps2Features( 52 | resp.features, 53 | activeProject, 54 | activeClass, 55 | activeEncodeImageItem.id 56 | ); 57 | const olFeatures = features2olFeatures(features); 58 | // Add items 59 | dispatchSetItems({ 60 | type: "SET_ITEMS", 61 | payload: [...items, ...olFeatures], 62 | }); 63 | 64 | // save in iddexedDB 65 | features.forEach((feature) => { 66 | storeItems.addData(feature); 67 | }); 68 | 69 | setSpinnerLoading(false); 70 | }; 71 | 72 | return ( 73 |
    78 |
    79 | 87 |
    88 |
    89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/components/DecodeItems.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { MenuTemplate } from "./MenuTemplate"; 3 | import { BsLayoutWtf } from "react-icons/bs"; 4 | import { Decode } from "./Decode"; 5 | 6 | export const DecodeItems = () => { 7 | const [openMenu, setOpenMenu] = useState(true); 8 | return ( 9 | } 12 | openMenu={openMenu} 13 | setOpenMenu={setOpenMenu} 14 | > 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/DecodePointPromt.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from "react"; 2 | import { NotificationManager } from "react-notifications"; 3 | import Feature from "ol/Feature"; 4 | import Point from "ol/geom/Point"; 5 | // import VectorSource from "ol/source/Vector"; 6 | 7 | import { MainContext } from "../contexts/MainContext"; 8 | import { requestSegments } from "../utils/requests"; 9 | import { 10 | features2olFeatures, 11 | setProps2Features, 12 | olFeatures2Features, 13 | convertBbox3857to4326, 14 | } from "../utils/convert"; 15 | import { storeItems } from "../store/indexedDB"; 16 | import { guid } from "../utils/utils"; 17 | import { vectorPointSelector } from "./map/layers"; 18 | 19 | export const DecodePointPromt = () => { 20 | const { 21 | map, 22 | activeProject, 23 | activeClass, 24 | dispatchSetItems, 25 | items, 26 | activeEncodeImageItem, 27 | setSpinnerLoading, 28 | decoderType, 29 | dispatchDecoderType, 30 | } = useContext(MainContext); 31 | 32 | const [selectedTab, setSelectedTab] = useState("singlePolygon"); 33 | const [isForegroundPromtPoint, setIsForegroundPromtPoint] = useState(true); 34 | // const [points, setPoints] = useState([]); 35 | 36 | const setDecodeType = (decodeType) => { 37 | dispatchDecoderType({ 38 | type: "SET_DECODER_TYPE", 39 | payload: decodeType, 40 | }); 41 | }; 42 | 43 | // Add point to send request to SAM 44 | useEffect(() => { 45 | if (!map ) { 46 | return; 47 | } 48 | 49 | if (!activeEncodeImageItem) { 50 | console.log("Select an AOI for making predictions within it.") 51 | return; 52 | } 53 | 54 | const clickHandler = (e) => { 55 | const coordinates = e.coordinate; 56 | const point = new Feature({ 57 | geometry: new Point(coordinates), 58 | }); 59 | 60 | const color = isForegroundPromtPoint ? [46, 62, 255] : [253, 23, 23]; 61 | const label = isForegroundPromtPoint ? 1 : 0; 62 | point.setProperties({ 63 | px: Math.ceil(e.pixel[0]), 64 | py: Math.ceil(e.pixel[1]), 65 | color, 66 | label, 67 | }); 68 | const source = vectorPointSelector.getSource(); 69 | source.addFeature(point); 70 | setTimeout(() => { 71 | point.setStyle(null); 72 | vectorPointSelector.changed(); 73 | }, 500); 74 | }; 75 | 76 | map.on("click", clickHandler); 77 | return () => map.un("click", clickHandler); 78 | }, [map, decoderType, activeEncodeImageItem, isForegroundPromtPoint]); 79 | 80 | 81 | const requestPointPromt = async (actionType) => { 82 | const points = vectorPointSelector.getSource().getFeatures(); 83 | if (!map || !activeEncodeImageItem || points.length < 1) { 84 | NotificationManager.warning( 85 | `Please ensure an AOI and at least one point are selected for predictions.`, 86 | 3000 87 | ); 88 | return; 89 | } 90 | 91 | setSpinnerLoading(true); 92 | const featuresPoints = olFeatures2Features(points); 93 | const coordinatesArray = featuresPoints.map( 94 | (feature) => feature.geometry.coordinates 95 | ); 96 | const labelsArray = featuresPoints.map( 97 | (feature) => feature.properties.label 98 | ); 99 | 100 | const reqProps = { 101 | bbox: convertBbox3857to4326(activeEncodeImageItem.bbox), 102 | point_labels: labelsArray, 103 | point_coords: coordinatesArray, 104 | crs: "EPSG:4326", 105 | zoom: activeEncodeImageItem.zoom, 106 | id: activeEncodeImageItem.id, 107 | project: activeProject.properties.slug, 108 | action_type: actionType, 109 | return_format: "geojson", 110 | simplify_tolerance: 0.000002, 111 | area_val: 1, 112 | }; 113 | const resp = await requestSegments(reqProps, "segment_predictor"); 114 | const features = setProps2Features( 115 | resp.features, 116 | activeProject, 117 | activeClass, 118 | activeEncodeImageItem.id 119 | ); 120 | const olFeatures = features2olFeatures(features); 121 | dispatchSetItems({ 122 | type: "SET_ITEMS", 123 | payload: [...items, ...olFeatures], 124 | }); 125 | 126 | // setPoints([]); 127 | vectorPointSelector.getSource().clear(); 128 | const items_id = guid(); 129 | features.forEach((feature, index) => { 130 | feature.id = `${items_id}_${index}`; 131 | feature.properties.id = `${items_id}_${index}`; 132 | storeItems.addData(feature); 133 | }); 134 | setSpinnerLoading(false); 135 | }; 136 | 137 | const tagChangeHandler = (type) => { 138 | vectorPointSelector.getSource().clear(); 139 | if (type === "single_point") { 140 | setSelectedTab("singlePolygon"); 141 | setDecodeType("single_point"); 142 | } else { 143 | setSelectedTab("multiPolygon"); 144 | setDecodeType("multi_point"); 145 | 146 | setIsForegroundPromtPoint(true) 147 | } 148 | 149 | } 150 | 151 | return ( 152 |
    153 | {/* Tab Navigation */} 154 |
    155 | 167 | 177 |
    178 | 179 | 180 | {/* Tab Content based on Selected Tab */} 181 |
    182 | {selectedTab === "singlePolygon" && ( 183 | <> 184 | 185 | {/* Foreground and Background Buttons */} 186 |
    187 | 194 | 201 |
    202 | 208 | 209 | 210 | )} 211 | {selectedTab === "multiPolygon" && ( 212 | 218 | )} 219 |
    220 |
    221 | ); 222 | }; -------------------------------------------------------------------------------- /src/components/EncodeCanvas.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { getCanvasForLayer } from "../utils/canvas"; 4 | import { getPropertiesRequest, setAOI } from "../utils/requests"; 5 | import { convertBbox4326to3857 } from "../utils/convert"; 6 | import { guid } from "../utils/utils"; 7 | 8 | export const EncodeCanvas = () => { 9 | const { 10 | map, 11 | pointsSelector, 12 | activeProject, 13 | encodeItems, 14 | dispatchEncodeItems, 15 | dispatchActiveEncodeImageItem, 16 | setSpinnerLoading, 17 | } = useContext(MainContext); 18 | 19 | const reset = () => { 20 | setSpinnerLoading(false); 21 | }; 22 | 23 | const requestSAMAOI = async (requestProps) => { 24 | setSpinnerLoading(true); 25 | try { 26 | const canvas = await getCanvasForLayer(map, "main_layer"); 27 | 28 | const encodeItem = { 29 | ...requestProps, 30 | canvas, 31 | id: guid(), 32 | project: activeProject.properties.slug, 33 | }; 34 | 35 | const respEncodeItem = await setAOI(encodeItem); 36 | respEncodeItem.bbox = convertBbox4326to3857(respEncodeItem.bbox); 37 | const newEncodeItems = [...encodeItems, respEncodeItem]; 38 | 39 | dispatchEncodeItems({ 40 | type: "CACHING_ENCODED", 41 | payload: newEncodeItems, 42 | }); 43 | 44 | dispatchActiveEncodeImageItem({ 45 | type: "SET_ACTIVE_ENCODE_IMAGE", 46 | payload: respEncodeItem, 47 | }); 48 | } catch (error) { 49 | reset(); 50 | } 51 | reset(); 52 | }; 53 | 54 | const requestAOI = async () => { 55 | const requestProps = getPropertiesRequest(map, pointsSelector); 56 | requestSAMAOI(requestProps); 57 | }; 58 | 59 | return ( 60 | <> 61 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/EncodeImport.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef } from "react"; 2 | import { NotificationManager } from "react-notifications"; 3 | 4 | import { MainContext } from "../contexts/MainContext"; 5 | import { storeEncodeItems } from "../store/indexedDB"; 6 | 7 | const EncodeImport = () => { 8 | const { dispatchEncodeItems } = useContext(MainContext); 9 | 10 | const fileInput = useRef(null); 11 | 12 | const handleFileChange = (e) => { 13 | const file = e.target.files[0]; 14 | if (!file) return; 15 | 16 | const reader = new FileReader(); 17 | 18 | reader.onload = (evt) => { 19 | const encodeItems = JSON.parse(evt.target.result); 20 | if ( 21 | encodeItems.length > 0 && 22 | encodeItems[0].id && 23 | encodeItems[0].image_shape 24 | ) { 25 | dispatchEncodeItems({ 26 | type: "CACHING_ENCODED", 27 | payload: encodeItems, 28 | }); 29 | //Save data in indexDB 30 | encodeItems.forEach((e) => storeEncodeItems.addData({ ...e })); 31 | } else { 32 | NotificationManager.error( 33 | "The imported file does not contain the required format.", 34 | `File format error`, 35 | 10000 36 | ); 37 | } 38 | }; 39 | 40 | reader.onerror = () => { 41 | NotificationManager.error(`File reading error`, 10000); 42 | }; 43 | 44 | reader.readAsText(file); 45 | }; 46 | 47 | const handleButtonClick = () => { 48 | fileInput.current.click(); 49 | }; 50 | 51 | return ( 52 |
    53 | 60 | 66 |
    67 | ); 68 | }; 69 | 70 | export default EncodeImport; 71 | -------------------------------------------------------------------------------- /src/components/EncodeItem.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { BsTrash } from "react-icons/bs"; 4 | import { storeEncodeItems } from "./../store/indexedDB"; 5 | 6 | export const EncodeItem = ({ encodeItem }) => { 7 | const { 8 | map, 9 | encodeItems, 10 | dispatchEncodeItems, 11 | activeEncodeImageItem, 12 | dispatchActiveEncodeImageItem, 13 | } = useContext(MainContext); 14 | 15 | const zoomTo = (encodeItem) => { 16 | if (!map) return; 17 | const { bbox, zoom } = encodeItem; 18 | map.getView().setZoom(zoom); 19 | map.getView().fit(bbox, { padding: [5, 5, 5, 5] }); 20 | // Set as active encode image items 21 | dispatchActiveEncodeImageItem({ 22 | type: "SET_ACTIVE_ENCODE_IMAGE", 23 | payload: encodeItem, 24 | }); 25 | }; 26 | 27 | const handleRemoveEncodeItem = async (encodeItem) => { 28 | if (activeEncodeImageItem && encodeItem.id === activeEncodeImageItem.id) { 29 | dispatchActiveEncodeImageItem({ 30 | type: "SET_ACTIVE_ENCODE_IMAGE", 31 | payload: null, 32 | }); 33 | } 34 | 35 | const updatedEncodeItems = encodeItems.filter((item, i) => { 36 | return item.id !== encodeItem.id; 37 | }); 38 | 39 | //Remove if is the only image to delete 40 | if (encodeItems.length === 1) { 41 | dispatchActiveEncodeImageItem({ 42 | type: "SET_ACTIVE_ENCODE_IMAGE", 43 | payload: null, 44 | }); 45 | } 46 | 47 | dispatchEncodeItems({ 48 | type: "CACHING_ENCODED", 49 | payload: updatedEncodeItems, 50 | }); 51 | try { 52 | storeEncodeItems.deleteData(encodeItem.id); 53 | } catch (error) { 54 | console.error("Failed to delete encode item:", error); 55 | } 56 | }; 57 | 58 | return ( 59 |
    60 | zoomTo(encodeItem)} 73 | /> 74 |
    81 | 82 | {activeEncodeImageItem && activeEncodeImageItem.id === encodeItem.id 83 | ? "Active" 84 | : ""} 85 | 86 |
    87 |
    88 | handleRemoveEncodeItem(encodeItem)} 90 | className="text-white fill-current w-3 h-3 cursor-pointer" 91 | /> 92 |
    93 |
    94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/components/EncodeItems.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { EncodeItem } from "./EncodeItem"; 4 | import { EncodeCanvas } from "./EncodeCanvas"; 5 | import { MenuTemplate } from "./MenuTemplate"; 6 | import { requestEncodeImages } from "../utils/requests"; 7 | import { convertBbox4326to3857 } from "../utils/convert"; 8 | 9 | import { BsLayoutWtf } from "react-icons/bs"; 10 | 11 | export const Badge = () => { 12 | const { activeEncodeImageItem } = useContext(MainContext); 13 | return ( 14 | 15 | {activeEncodeImageItem 16 | ? `z${Math.round(activeEncodeImageItem.zoom)}` 17 | : ""} 18 | 19 | ); 20 | }; 21 | 22 | export const EncodeItems = () => { 23 | const { encodeItems, dispatchEncodeItems, activeProject } = 24 | useContext(MainContext); 25 | const [openMenu, setOpenMenu] = useState(true); 26 | 27 | // Load indexedDB for encode Items 28 | useEffect(() => { 29 | if (!activeProject) return; 30 | const fetchData = async () => { 31 | try { 32 | const encodeImages = await requestEncodeImages( 33 | activeProject.properties.slug 34 | ); 35 | let encodedImagesArray = Object.values(encodeImages.detection).map( 36 | (encodeI) => { 37 | encodeI.bbox = convertBbox4326to3857(encodeI.bbox); 38 | return encodeI; 39 | } 40 | ); 41 | 42 | dispatchEncodeItems({ 43 | type: "CACHING_ENCODED", 44 | payload: encodedImagesArray, 45 | }); 46 | } catch (error) { 47 | console.log(error); 48 | } 49 | }; 50 | fetchData(); 51 | setOpenMenu(true); 52 | }, [activeProject]); 53 | 54 | return ( 55 | } 58 | icon={} 59 | openMenu={openMenu} 60 | setOpenMenu={setOpenMenu} 61 | > 62 | <> 63 |
    64 | {encodeItems.map((encodeItem, index) => ( 65 | 66 | ))} 67 | {!encodeItems?.length && ( 68 |

    69 | No AOI is available yet 70 |

    71 | )} 72 |
    73 |
    74 | 75 |
    76 | 77 |
    78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { BsInfoCircleFill } from "react-icons/bs"; 3 | 4 | import { MainContext } from "./../contexts/MainContext"; 5 | import DevSeedLogo from "./../media/layout/ds-logo-pos.svg"; 6 | 7 | export const Header = () => { 8 | const { setDisplayModal } = useContext(MainContext); 9 | return ( 10 |
    11 | 12 | Development Seed logo 17 | 18 | 19 | DS-Annotate 20 | 21 |
    22 | { 24 | setDisplayModal("block"); 25 | }} 26 | /> 27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/Item.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { BsTrash } from "react-icons/bs"; 3 | import { MainContext } from "./../contexts/MainContext"; 4 | import { storeItems } from "./../store/indexedDB"; 5 | 6 | const Item = ({ item, index }) => { 7 | const { items, dispatchSetItems, dispatchSetHighlightedItem } = 8 | useContext(MainContext); 9 | 10 | const deleteItem = (item) => { 11 | const newItems = items.filter((i) => { 12 | return i.values_.id !== item.values_.id; 13 | }); 14 | dispatchSetItems({ 15 | type: "SET_ITEMS", 16 | payload: newItems, 17 | }); 18 | // Delete from DB 19 | storeItems.deleteData(item.values_.id); 20 | }; 21 | 22 | const zoomToItem = (item) => { 23 | dispatchSetHighlightedItem({ 24 | type: "SET_HIGHLIGHTED_ITEM", 25 | payload: item, 26 | }); 27 | }; 28 | return ( 29 |
    { 33 | zoomToItem(item); 34 | }} 35 | onMouseLeave={() => { 36 | zoomToItem({}); 37 | }} 38 | > 39 | { 42 | deleteItem(item); 43 | zoomToItem(null); 44 | }} 45 | title="Delete item" 46 | > 47 | {index} 48 |
    49 | ); 50 | }; 51 | 52 | export default Item; 53 | -------------------------------------------------------------------------------- /src/components/Items.js: -------------------------------------------------------------------------------- 1 | import { 2 | useContext, 3 | useEffect, 4 | useLayoutEffect, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import { MainContext } from "../contexts/MainContext"; 9 | import Item from "./Item"; 10 | import { openDatabase, storeItems } from "../store/indexedDB"; 11 | import { features2olFeatures } from "../utils/convert"; 12 | import { MenuTemplate } from "./MenuTemplate"; 13 | import React from "react"; 14 | import { ItemsDataActions } from "./ItemsDataActions"; 15 | import { BsBoundingBoxCircles } from "react-icons/bs"; 16 | 17 | export const Items = () => { 18 | const { items, dispatchSetItems, activeProject } = useContext(MainContext); 19 | const scrollDivRef = useRef(null); 20 | const [openMenu, setOpenMenu] = useState(true); 21 | 22 | // Load items data from DB 23 | useLayoutEffect(() => { 24 | if (!activeProject) return; 25 | const fetchData = async () => { 26 | try { 27 | await openDatabase(); 28 | const items_ = await storeItems.getDataByProject( 29 | activeProject.properties.name 30 | ); 31 | const filterItems_ = items_.filter( 32 | (item) => item.geometry.coordinates.length > 0 33 | ); 34 | const olFeatures = features2olFeatures(filterItems_); 35 | dispatchSetItems({ 36 | type: "SET_ITEMS", 37 | payload: olFeatures, 38 | }); 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }; 43 | fetchData(); 44 | setOpenMenu(true); 45 | }, [activeProject]); 46 | 47 | useEffect(() => { 48 | if (scrollDivRef.current) { 49 | const scrollDiv = scrollDivRef.current; 50 | scrollDiv.scrollTop = scrollDiv.scrollHeight; 51 | } 52 | }, [items]); 53 | return ( 54 | } 57 | openMenu={openMenu} 58 | setOpenMenu={setOpenMenu} 59 | > 60 |
    64 | {items.map((item, index) => { 65 | return ; 66 | })} 67 |
    68 |
    69 | 70 |
    71 |
    72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/ItemsDataActions.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useCallback } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { 4 | mergePolygonClass, 5 | simplifyFeatures, 6 | smooth, 7 | } from "../utils/transformation"; 8 | import { olFeatures2Features, features2olFeatures } from "../utils/convert"; 9 | import { storeItems } from "../store/indexedDB"; 10 | 11 | export const ItemsDataActions = () => { 12 | const { items, dispatchSetItems, activeProject, activeEncodeImageItem } = 13 | useContext(MainContext); 14 | 15 | const setItems = useCallback( 16 | (items) => { 17 | dispatchSetItems({ 18 | type: "SET_ITEMS", 19 | payload: items, 20 | }); 21 | }, 22 | [dispatchSetItems] 23 | ); 24 | 25 | const mergePolygons = () => { 26 | const features = olFeatures2Features(items); 27 | const mergedFeatures = mergePolygonClass(features); 28 | const smoothFeatures = smooth(mergedFeatures); 29 | const simpFeatures = simplifyFeatures(smoothFeatures, 0.000002); 30 | const mergedItems = features2olFeatures(simpFeatures); 31 | setItems(mergedItems); 32 | // Save merged features in DB 33 | storeItems.deleteDataByProject(activeProject.properties.name); 34 | mergedFeatures.forEach((item) => { 35 | storeItems.addData({ 36 | ...item, 37 | id: item.properties.id, 38 | project: activeProject.properties.name, 39 | }); 40 | }); 41 | }; 42 | 43 | return ( 44 |
    45 | 48 |
    49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/MenuExpData.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { MainContext } from "../contexts/MainContext"; 3 | import { downloadInJOSM } from "../utils/requests"; 4 | import { downloadJsonFile, guid } from "../utils/utils"; 5 | import { olFeatures2geojson } from "../utils/convert"; 6 | import { BsDownload, BsUpload } from "react-icons/bs"; 7 | 8 | export const MenuExpData = () => { 9 | const { items, activeProject, activeEncodeImageItem } = 10 | useContext(MainContext); 11 | 12 | const downloadGeojson = () => { 13 | const geojson = JSON.stringify(olFeatures2geojson(items)); 14 | const projectName = activeProject.properties.name.replace(/\s/g, "_"); 15 | downloadJsonFile(geojson, `${projectName}.geojson`); 16 | }; 17 | 18 | const josm = () => { 19 | const geojson = JSON.stringify(olFeatures2geojson(items)); 20 | let aoiId = guid(); 21 | if (activeEncodeImageItem) { 22 | aoiId = activeEncodeImageItem.id; 23 | } 24 | downloadInJOSM(geojson, activeProject, aoiId); 25 | }; 26 | 27 | return ( 28 |
    29 | 39 | 45 |
    46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/MenuTemplate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BsChevronDown, BsChevronUp } from "react-icons/bs"; 3 | 4 | export const MenuTemplate = ({ 5 | title, 6 | badge, 7 | icon, 8 | openMenu, 9 | setOpenMenu, 10 | children, 11 | }) => { 12 | return ( 13 | <> 14 |
    setOpenMenu(!openMenu)}> 15 | {icon} 16 | 17 | {title} 18 | 19 | {badge} 20 | {openMenu ? : } 21 |
    22 | {openMenu &&
    {children}
    } 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { MainContext } from "./../contexts/MainContext"; 3 | import { BsXLg } from "react-icons/bs"; 4 | 5 | export const Modal = () => { 6 | const { displayModal, setDisplayModal } = useContext(MainContext); 7 | 8 | const modalStatus = (value) => { 9 | localStorage.setItem("modalDisplay", value); 10 | setDisplayModal(value); 11 | }; 12 | 13 | if (displayModal === "block") { 14 | return ( 15 |
    16 |
    17 |
    18 | {/*content*/} 19 |
    20 | {/*header*/} 21 |
    22 |

    DS-Annotate

    23 | 24 | modalStatus("none")} 27 | /> 28 |
    29 | {/*body*/} 30 |
    31 |

    32 | DS-Annotate empowers users to draw intricate polygons over 33 | aerial imagery using the{" "} 34 | 40 | {`Segment Anything Model (SAM)`} 41 | 42 | and the{" "} 43 | 49 | {` Magic Wand `} 50 | {" "} 51 | functionality. DS-Annotate simplifies the process of creating 52 | complex polygons on maps, making it an essential asset for 53 | detailed annotation. 54 |

    55 |

    56 | To get started, follow these steps: 57 |

    58 |
      59 |
    • 60 | Select the desired project and class from the sidebar menu. 61 |
    • 62 |
    • 63 | Wait until all imagery tiles are loaded in the map. 64 |
    • 65 |
    66 | 67 |

    68 | Once your workspace is set up, you can begin annotating in two 69 | ways: 70 |

    71 | 72 |
      73 |
    • 74 | Magic Wand: Right-click 75 | on the area you want to select. You can click and drag your 76 | mouse up/down to adjust the thresold. After making your 77 | selection, press the{" "} 78 | 79 | s 80 | {" "} 81 | key to store the polygon. 82 |
    • 83 |
    • 84 | Segment Anything Model: 85 | Click on the element in the imagery you want to get mapped. 86 | Then, press the Segment Anything button to activate 87 | the model and start getting the polygons. If you want more 88 | elements mapped, just click on them. 89 |
    • 90 |
    91 | 92 |

    93 | Both ways offer precise and efficient mapping, enhancing the 94 | overall annotation experience. 95 |

    96 | 97 |

    98 | You can then download the polygons in GeoJSON format or export 99 | it to the Java OpenStreetMap Editor (JOSM). 100 |

    101 | 102 |

    103 | 109 | Give us some feedback 110 | 111 | . 112 |

    113 |
    114 |
    115 | 122 |
    123 |
    124 |
    125 |
    126 |
    127 |
    128 | ); 129 | } 130 | return false; 131 | }; 132 | -------------------------------------------------------------------------------- /src/components/Projects.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect, useCallback } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useSearchParams } from "react-router-dom"; 4 | import { MainContext } from "../contexts/MainContext"; 5 | import { getClassLayers } from "../utils/convert"; 6 | import { getProjectTemplate } from "../utils/utils"; 7 | import { CreateProjectModal } from "./CreateProjectModal"; 8 | 9 | import { MenuTemplate } from "./MenuTemplate"; 10 | import { BsFolder2 } from "react-icons/bs"; 11 | 12 | export const Projects = () => { 13 | const { 14 | projects, 15 | dispatchSetActiveProject, 16 | dispatchSetActiveClass, 17 | dispatchActiveEncodeImageItem, 18 | } = useContext(MainContext); 19 | const [projectName, setProjectName] = useState("Projects"); 20 | const [isOpen, setIsOpen] = useState(false); 21 | const [isModalOpen, setIsModalOpen] = useState(false); 22 | 23 | const [newProjectName, setNewProjectName] = useState(""); // State for new project name 24 | 25 | const setProject = useCallback( 26 | (project) => { 27 | // Set active project 28 | dispatchSetActiveProject({ 29 | type: "SET_ACTIVE_PROJECT", 30 | payload: project, 31 | }); 32 | 33 | // Set Active class the first in the list 34 | const classLayers = getClassLayers(project); 35 | dispatchSetActiveClass({ 36 | type: "SET_ACTIVE_CLASS", 37 | payload: classLayers[0], 38 | }); 39 | 40 | setProjectName(project.properties.name); 41 | 42 | // Set active encode image to null 43 | dispatchActiveEncodeImageItem({ 44 | type: "SET_ACTIVE_ENCODE_IMAGE", 45 | payload: null, 46 | }); 47 | }, 48 | [dispatchSetActiveClass, dispatchSetActiveProject] 49 | ); 50 | 51 | // Load project from query 52 | const [searchParams] = useSearchParams(); 53 | 54 | useEffect(() => { 55 | let projectFeature = getProjectTemplate(searchParams); 56 | if (!projectFeature) { 57 | // If the project was not set in the URL, find the projects by slug on the existing list, 58 | const projectSlug = searchParams.get("project"); 59 | if (projectSlug) { 60 | const listProjectFound = projects.features.filter((p) => { 61 | return p.properties.slug === projectSlug; 62 | }); 63 | if (listProjectFound.length > 0) { 64 | projectFeature = listProjectFound[0]; 65 | } 66 | } 67 | } 68 | if (projectFeature) { 69 | setProject(projectFeature); 70 | } 71 | }, [searchParams]); 72 | 73 | // Function to handle creating a new project 74 | const handleCreateNewProject = () => { 75 | if (!newProjectName.trim()) return alert("Project name cannot be empty!"); 76 | 77 | // Example: Add a new project to the list 78 | const newProject = { 79 | properties: { 80 | name: newProjectName, 81 | slug: newProjectName.toLowerCase().replace(/\s+/g, "-"), 82 | }, 83 | }; 84 | 85 | // Update the projects list in MainContext (this is an example; adjust based on your context logic) 86 | const updatedProjects = { 87 | ...projects, 88 | features: [...projects.features, newProject], 89 | }; 90 | 91 | // Assuming there is a function to update projects in the context 92 | dispatchSetActiveProject({ 93 | type: "UPDATE_PROJECTS", 94 | payload: updatedProjects, 95 | }); 96 | 97 | // Reset the input field 98 | setNewProjectName(""); 99 | 100 | // Optionally set the new project as active 101 | setProject(newProject); 102 | }; 103 | 104 | return ( 105 | } 109 | openMenu={isOpen} 110 | setOpenMenu={setIsOpen} 111 | > 112 |
    113 |
      114 | {projects.features.map((feature) => ( 115 | { 123 | setProject(feature); 124 | setIsOpen(false); 125 | }} 126 | to={`?project=${feature.properties.slug}`} 127 | > 128 | {feature.properties.name} 129 | 130 | ))} 131 |
    132 | {/* Add New Project Section */} 133 |
    134 |
    135 | {/* Button to open the modal */} 136 | 142 |
    143 | 144 | {/* Modal Component */} 145 | setIsModalOpen(false)} 148 | // onSubmit={handleFormSubmit} 149 | // formData={formData} 150 | // setFormData={setFormData} 151 | /> 152 |
    153 |
    154 |
    155 | ); 156 | }; -------------------------------------------------------------------------------- /src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Header } from "./Header"; 4 | import { Projects } from "./Projects"; 5 | import { Classes } from "./Classes"; 6 | import { Items } from "./Items"; 7 | import { MenuExpData } from "./MenuExpData"; 8 | import { EncodeItems } from "./EncodeItems"; 9 | import { DecodeItems } from "./DecodeItems"; 10 | 11 | export const Sidebar = () => { 12 | return ( 13 | <> 14 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/SpinerLoader.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { ScaleLoader } from "react-spinners"; 3 | import { MainContext } from "../contexts/MainContext"; 4 | 5 | export const SpinerLoader = () => { 6 | const { spinnerLoading } = useContext(MainContext); 7 | return ( 8 | <> 9 | {spinnerLoading ? ( 10 |
    11 | 17 |
    18 | ) : null} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/map/MagicWand.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { Feature } from "ol"; 3 | import Polygon from "ol/geom/Polygon"; 4 | 5 | import { MainContext } from "../../contexts/MainContext"; 6 | import { guid } from "./../../utils/utils"; 7 | import { features2olFeatures, olFeatures2Features } from "../../utils/convert"; 8 | import { storeItems } from "./../../store/indexedDB"; 9 | 10 | export const MagicWand = ({ event }) => { 11 | const { map, wand, activeProject, activeClass, items, dispatchSetItems } = 12 | useContext(MainContext); 13 | 14 | useEffect(() => { 15 | const drawSelection = async () => { 16 | if (event && event.type === "keypress" && event.key === "s") { 17 | let contours = wand.getContours(); 18 | if (!contours) return; 19 | let rings = contours.map((c) => 20 | c.points.map((p) => map.getCoordinateFromPixel([p.x, p.y])) 21 | ); 22 | if (rings.length === 0) return; 23 | try { 24 | const id = guid(); 25 | const oLFeature = new Feature({ 26 | geometry: new Polygon(rings), 27 | project: activeProject.properties.name, 28 | class: activeClass.name, 29 | color: activeClass.color, 30 | id: id, 31 | }); 32 | 33 | //Simplify features 34 | const features = olFeatures2Features([oLFeature]); 35 | 36 | //Insert the first items 37 | const feature = features[0]; 38 | const oLFeatures = features2olFeatures([feature]); 39 | 40 | dispatchSetItems({ 41 | type: "SET_ITEMS", 42 | payload: [...items, oLFeatures[0]], 43 | }); 44 | 45 | //Insert feature into the DB 46 | await storeItems.addData({ 47 | ...feature, 48 | id, 49 | project: activeProject.properties.name, 50 | }); 51 | wand.clearMask(); 52 | } catch (error) { 53 | console.log(error); 54 | } 55 | } 56 | }; 57 | drawSelection(); 58 | }, [event]); 59 | 60 | return null; 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/map/ProjectLayer.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useContext } from "react"; 2 | import VectorSource from "ol/source/Vector"; 3 | import { GeoJSON } from "ol/format"; 4 | import * as turf from "@turf/turf"; 5 | 6 | import { MainContext } from "../../contexts/MainContext"; 7 | 8 | import { 9 | vector, 10 | mainLayer, 11 | getImageryLayer, 12 | vectorSegData, 13 | vectorHighlighted, 14 | encodeMapViews, 15 | encodeMapViewHighlighted, 16 | } from "./layers"; 17 | 18 | import { bbox2polygon, getOLFeatures } from "../../utils/convert"; 19 | 20 | const PADDING = { padding: [100, 100, 100, 100] }; 21 | 22 | export const ProjectLayer = ({ project, items, highlightedItem }) => { 23 | const { 24 | map, 25 | pointsSelector, 26 | dispatchSetPointsSelector, 27 | encodeItems, 28 | activeEncodeImageItem, 29 | decoderType, 30 | dispatchDecoderType, 31 | } = useContext(MainContext); 32 | 33 | useEffect(() => { 34 | if (!map) return; 35 | // if (pointsSelector.length === 0) return; 36 | if (project) { 37 | const geojsonSource = new VectorSource({ 38 | features: new GeoJSON({ featureProjection: "EPSG:3857" }).readFeatures( 39 | turf.featureCollection([project]) 40 | ), 41 | }); 42 | vector.setSource(geojsonSource); 43 | map.getView().fit(geojsonSource.getExtent(), PADDING); 44 | } 45 | 46 | return () => { 47 | if (map) map.getView().fit([0, 0, 0, 0], PADDING); 48 | }; 49 | }, [map, project]); 50 | 51 | useEffect(() => { 52 | if (!map) return; 53 | if (project && project.properties && project.properties.imagery && map) { 54 | mainLayer.setSource(getImageryLayer(project.properties.imagery)); 55 | } 56 | 57 | return () => { 58 | if (map) mainLayer.setSource(null); 59 | }; 60 | }, [map, project]); 61 | 62 | useEffect(() => { 63 | if (!map) return; 64 | try { 65 | if (items.length >= 0) { 66 | const segDataSource = new VectorSource({ 67 | features: items, 68 | wrapX: true, 69 | }); 70 | vectorSegData.setSource(segDataSource); 71 | } 72 | } catch (error) { 73 | console.log(error); 74 | } 75 | }, [map, items]); 76 | 77 | useEffect(() => { 78 | if (!map) return; 79 | if (!highlightedItem) highlightedItem = []; 80 | const segDataSource = new VectorSource({ 81 | features: 82 | Object.keys(highlightedItem).length !== 0 ? [highlightedItem] : [], 83 | wrapX: true, 84 | }); 85 | vectorHighlighted.setSource(segDataSource); 86 | }, [map, highlightedItem]); 87 | 88 | useEffect(() => { 89 | if (!map) return; 90 | map.on("moveend", function (e) { 91 | map.updateSize(); 92 | }); 93 | }, [map]); 94 | 95 | // Update vector layer to desplay the bbox of the decode images 96 | useEffect(() => { 97 | if (!map) return; 98 | const features = encodeItems.map((ei) => { 99 | return bbox2polygon(ei.bbox); 100 | }); 101 | 102 | const olFeatures = getOLFeatures(features); 103 | const dataSource = new VectorSource({ 104 | features: olFeatures, 105 | wrapX: true, 106 | }); 107 | encodeMapViews.setSource(dataSource); 108 | }, [encodeItems]); 109 | 110 | useEffect(() => { 111 | if (!map) return; 112 | let features = []; 113 | if (activeEncodeImageItem) { 114 | features = [bbox2polygon(activeEncodeImageItem.bbox)]; 115 | } 116 | 117 | const olFeatures = getOLFeatures(features); 118 | encodeMapViewHighlighted.setSource( 119 | new VectorSource({ 120 | features: olFeatures, 121 | wrapX: true, 122 | }) 123 | ); 124 | }, [activeEncodeImageItem]); 125 | 126 | return null; 127 | }; 128 | -------------------------------------------------------------------------------- /src/components/map/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | useEffect, 4 | useRef, 5 | useLayoutEffect, 6 | useContext, 7 | } from "react"; 8 | import T from "prop-types"; 9 | import Map from "ol/Map"; 10 | import View from "ol/View"; 11 | import { defaults as defaultControls } from "ol/control"; 12 | import MagicWandInteraction from "ol-magic-wand"; 13 | import "ol/ol.css"; 14 | import { 15 | Modify, 16 | Select, 17 | defaults as defaultInteractions, 18 | } from "ol/interaction"; 19 | 20 | import { 21 | osm, 22 | vector, 23 | mainLayer, 24 | vectorSegData, 25 | vectorHighlighted, 26 | vectorPointSelector, 27 | encodeMapViews, 28 | encodeMapViewHighlighted, 29 | } from "./layers"; 30 | import { MainContext } from "../../contexts/MainContext"; 31 | import { ProjectLayer } from "./ProjectLayer"; 32 | import { MagicWand } from "./MagicWand"; 33 | 34 | export function MapWrapper({ children }) { 35 | // Import values from main context 36 | const { 37 | map, 38 | setMap, 39 | setWand, 40 | activeProject, 41 | items, 42 | dispatchSetItems, 43 | highlightedItem, 44 | } = useContext(MainContext); 45 | 46 | const mapElement = useRef(); 47 | const mapRef = useRef(); 48 | mapRef.current = map; 49 | 50 | // State for events on the map 51 | const [event, setEvent] = useState(null); 52 | 53 | // Initialize all the map components 54 | useLayoutEffect(() => { 55 | const initWand = new MagicWandInteraction({ 56 | layers: [mainLayer], 57 | hatchLength: 4, 58 | hatchTimeout: 300, 59 | waitClass: "magic-wand-loading", 60 | addClass: "magic-wand-add", 61 | }); 62 | const view = new View({ 63 | projection: "EPSG:3857", 64 | center: [0, 0], 65 | zoom: 2, 66 | }); 67 | 68 | const interactions = { 69 | doubleClickZoom: true, 70 | keyboardPan: false, 71 | keyboardZoom: false, 72 | mouseWheelZoom: false, 73 | pointer: false, 74 | select: false, 75 | }; 76 | 77 | const select = new Select({ 78 | wrapX: false, 79 | }); 80 | 81 | const modify = new Modify({ 82 | features: select.getFeatures(), 83 | }); 84 | 85 | const initialMap = new Map({ 86 | target: mapElement.current, 87 | controls: defaultControls().extend([]), 88 | interactions: defaultInteractions(interactions).extend([ 89 | initWand, 90 | select, 91 | modify, 92 | ]), 93 | layers: [ 94 | osm, 95 | mainLayer, 96 | vector, 97 | vectorSegData, 98 | vectorHighlighted, 99 | vectorPointSelector, 100 | encodeMapViews, 101 | encodeMapViewHighlighted, 102 | ], 103 | view: view, 104 | }); 105 | 106 | // // Add hash map in the url 107 | // initialMap.on("moveend", function () { 108 | // const view = initialMap.getView(); 109 | // const zoom = view.getZoom(); 110 | // const coord = view.getCenter(); 111 | // window.location.hash = `#map=${Math.round(zoom)}/${coord[0]}/${coord[1]}`; 112 | // }); 113 | 114 | setMap(initialMap); 115 | setWand(initWand); 116 | }, []); 117 | 118 | useEffect(() => { 119 | if (activeProject) { 120 | dispatchSetItems({ 121 | type: "SET_ITEMS", 122 | payload: [], 123 | }); 124 | } 125 | }, [activeProject]); 126 | 127 | const drawSegments = (e) => { 128 | setEvent(e); 129 | }; 130 | 131 | const handleOnKeyDown = (e) => { 132 | //Fetch predition from SAM 133 | }; 134 | 135 | const handleClick = (e) => {}; 136 | 137 | return ( 138 | <> 139 |
    148 | 149 | {activeProject && ( 150 | 155 | )} 156 | {children} 157 |
    158 | 159 | ); 160 | } 161 | 162 | MapWrapper.propTypes = { 163 | project: T.object, 164 | children: T.node, 165 | }; 166 | -------------------------------------------------------------------------------- /src/components/map/layers.js: -------------------------------------------------------------------------------- 1 | import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer"; 2 | import { TileWMS, OSM, XYZ } from "ol/source"; 3 | import { Circle as CircleStyle, Fill, Stroke, Style } from "ol/style"; 4 | import VectorSource from "ol/source/Vector"; 5 | import Icon from "ol/style/Icon"; 6 | import foregroundIcon from './../../media/icons/foreground.svg'; 7 | import backgroundIcon from './../../media/icons/background.svg'; 8 | 9 | import MultiPoint from "ol/geom/MultiPoint"; 10 | import { convertColorToRGBA } from "../../utils/utils"; 11 | export const osm = new TileLayer({ 12 | source: new OSM(), 13 | zIndex: 1, 14 | }); 15 | 16 | export const vector = new VectorLayer({ 17 | style: new Style({ 18 | stroke: new Stroke({ 19 | color: "#dd1c77", 20 | width: 3, 21 | }), 22 | }), 23 | zIndex: 5, 24 | }); 25 | 26 | export const vectorSegData = new VectorLayer({ 27 | style: function (feature) { 28 | const colorCode = feature.get("color"); 29 | const transparency = 0.1; 30 | return [ 31 | new Style({ 32 | background: "white", 33 | stroke: new Stroke({ 34 | color: colorCode, 35 | width: 1, 36 | }), 37 | }), 38 | new Style({ 39 | image: new CircleStyle({ 40 | radius: 1.2, 41 | fill: new Fill({ 42 | color: colorCode, 43 | }), 44 | }), 45 | geometry: function (feature) { 46 | // Return the coordinates of the first ring of the polygon 47 | const coordinates = feature.getGeometry().getCoordinates()[0]; 48 | return new MultiPoint(coordinates); 49 | }, 50 | }), 51 | new Style({ 52 | fill: new Fill({ 53 | color: convertColorToRGBA(colorCode, transparency), 54 | }), 55 | }), 56 | ]; 57 | }, 58 | zIndex: 5, 59 | }); 60 | 61 | export const vectorHighlighted = new VectorLayer({ 62 | style: new Style({ 63 | stroke: new Stroke({ 64 | width: 3, 65 | color: [209, 51, 255, 1], 66 | }), 67 | fill: new Fill({ 68 | color: [209, 51, 255, 0.3], 69 | }), 70 | }), 71 | zIndex: 5, 72 | }); 73 | 74 | export const mainLayer = new TileLayer({ zIndex: 2, title: "main_layer" }); 75 | 76 | export const getImageryLayer = (imagery) => { 77 | if (imagery.type === "wms") { 78 | return new TileWMS({ 79 | url: imagery.url, 80 | // params: { LAYERS: imagery.layerName, TILED: true }, 81 | params: imagery.params, 82 | ratio: 1, 83 | serverType: imagery.serverType, 84 | crossOrigin: "anonymous", 85 | }); 86 | } 87 | 88 | if (imagery.type === "tms") { 89 | return new XYZ({ url: imagery.url, crossOrigin: "anonymous" }); 90 | } 91 | }; 92 | 93 | export const vectorPointSelector = new VectorLayer({ 94 | source: new VectorSource(), 95 | style: function (feature) { 96 | const iconUrl = feature.get("label") === 1 ? foregroundIcon : backgroundIcon; 97 | const defaultScale = 2; 98 | const scale = feature.get("size") ? feature.get("size") : defaultScale; 99 | return new Style({ 100 | image: new Icon({ 101 | src: iconUrl, 102 | scale: scale, 103 | anchor: [0.5, 1], 104 | anchorXUnits: "fraction", 105 | anchorYUnits: "fraction", 106 | }), 107 | }); 108 | }, 109 | zIndex: 10, 110 | }); 111 | 112 | export const encodeMapViews = new VectorLayer({ 113 | style: new Style({ 114 | stroke: new Stroke({ 115 | width: 2, 116 | color: [219, 154, 109, 1], 117 | }), 118 | }), 119 | zIndex: 7, 120 | }); 121 | 122 | export const encodeMapViewHighlighted = new VectorLayer({ 123 | style: new Style({ 124 | stroke: new Stroke({ 125 | width: 2, 126 | color: [0, 255, 97, 1], 127 | }), 128 | }), 129 | zIndex: 8, 130 | }); 131 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | export const SamGeoAPI = "https://samgeo-api.geocompas.ai"; 2 | export const indexedDBName = "dsAnnotateDB"; 3 | export const indexedDBVersion = 1; 4 | -------------------------------------------------------------------------------- /src/contexts/MainContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer, useState } from "react"; 2 | import propTypes from "prop-types"; 3 | 4 | import projects from "./../static/projects.json"; 5 | import { 6 | downloadGeojsonReducer, 7 | downloadInJOSMReducer, 8 | activeClassReducer, 9 | activeProjectReducer, 10 | itemsReducer, 11 | highlightedItemReducer, 12 | pointsSelectorReducer, 13 | encodeItemsReducer, 14 | activeEncodeImage, 15 | activeDecoderType, 16 | } from "./../reducers"; 17 | 18 | export const MainContext = createContext(); 19 | 20 | const MainContextProvider = (props) => { 21 | const [map, setMap] = useState(); 22 | 23 | const [wand, setWand] = useState(null); 24 | 25 | const [spinnerLoading, setSpinnerLoading] = useState(false); 26 | 27 | const [activeProject, dispatchSetActiveProject] = useReducer( 28 | activeProjectReducer, 29 | null 30 | ); 31 | 32 | const [activeClass, dispatchSetActiveClass] = useReducer( 33 | activeClassReducer, 34 | null 35 | ); 36 | 37 | const [items, dispatchSetItems] = useReducer(itemsReducer, []); 38 | 39 | const [highlightedItem, dispatchSetHighlightedItem] = useReducer( 40 | highlightedItemReducer, 41 | {} 42 | ); 43 | 44 | const [dlGeojson, dispatchDLGeojson] = useReducer( 45 | downloadGeojsonReducer, 46 | false 47 | ); 48 | 49 | const [dlInJOSM, dispatchDLInJOSM] = useReducer(downloadInJOSMReducer, false); 50 | 51 | const [pointsSelector, dispatchSetPointsSelector] = useReducer( 52 | pointsSelectorReducer, 53 | [] 54 | ); 55 | 56 | const [encodeItems, dispatchEncodeItems] = useReducer(encodeItemsReducer, []); 57 | 58 | const [activeEncodeImageItem, dispatchActiveEncodeImageItem] = useReducer( 59 | activeEncodeImage, 60 | null 61 | ); 62 | 63 | const [displayModal, setDisplayModal] = useState(() => { 64 | const saved = localStorage.getItem("modalDisplay"); 65 | if (!saved) return "block"; 66 | return "none"; 67 | }); 68 | 69 | const [decoderType, dispatchDecoderType] = useReducer( 70 | activeDecoderType, 71 | "none" 72 | ); 73 | 74 | return ( 75 | 108 | {props.children} 109 | 110 | ); 111 | }; 112 | 113 | MainContextProvider.propTypes = { 114 | children: propTypes.node, 115 | }; 116 | 117 | export default MainContextProvider; 118 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .menuHeader { 7 | @apply text-gray-800 text-xs sm:text-base flex items-center gap-x-2 cursor-pointer py-1 px-2 rounded-md mt-2 bg-slate-100; 8 | } 9 | .subMenuHeader { 10 | @apply text-gray-800 text-xs flex items-center gap-x-1 cursor-pointer p-1 pl-8 pt-1 pb-1 rounded-md mt-1; 11 | } 12 | .hoverAnimation { 13 | @apply hover:bg-slate-300 hover:bg-opacity-40 rounded-md cursor-pointer transition duration-200 ease-out; 14 | } 15 | .custom_button { 16 | @apply bg-transparent hover:bg-orange-ds hover:bg-opacity-60 text-orange-ds text-xs font-semibold hover:text-white py-1 px-1 border border-orange-ds hover:border-transparent rounded cursor-pointer; 17 | } 18 | } 19 | 20 | 21 | .notification-container { 22 | width: auto!important; 23 | min-width: 290px!important; 24 | font-size: 12px!important; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | import MainContextProvider from "./contexts/MainContext"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | const basename = process.env.PUBLIC_URL; 10 | root.render( 11 | 12 | 13 | 14 | } /> 15 | } /> 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/media/icons/background.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/media/icons/foreground.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/media/layout/ds-logo-pos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/media/layout/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/media/meta/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developmentseed/ds-annotate/e820a758e44ec6633aaaada690dfc04afe6cdc42/src/media/meta/favicon.png -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export const createReducer = 2 | (type, sortingFunction = null, defaultValue = null) => 3 | (state, action) => { 4 | if (action.type === type) { 5 | if (sortingFunction) { 6 | return action.payload.sort(sortingFunction); 7 | } else { 8 | return action.payload || defaultValue; 9 | } 10 | } 11 | return state; 12 | }; 13 | 14 | export const activeProjectReducer = createReducer("SET_ACTIVE_PROJECT"); 15 | 16 | export const activeClassReducer = createReducer("SET_ACTIVE_CLASS"); 17 | 18 | export const itemsReducer = createReducer("SET_ITEMS", null, []); 19 | 20 | export const highlightedItemReducer = createReducer("SET_HIGHLIGHTED_ITEM"); 21 | 22 | export const downloadGeojsonReducer = createReducer("DOWNLOAD_GEOJSON"); 23 | 24 | export const downloadInJOSMReducer = createReducer("DOWNLOAD_IN_JOSM"); 25 | 26 | export const ItemsNumClass = createReducer("SET_ITEM_ID"); 27 | 28 | export const encodeItemsReducer = createReducer("CACHING_ENCODED"); 29 | 30 | export const activeEncodeImage = createReducer("SET_ACTIVE_ENCODE_IMAGE"); 31 | 32 | export const pointsSelectorReducer = (state = [], action) => { 33 | switch (action.type) { 34 | case "SET_SINGLE_POINT": 35 | return [action.payload]; 36 | case "SET_MULTI_POINT": 37 | return [...state, action.payload]; 38 | case "SET_EMPTY_POINT": 39 | return []; 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | export const activeDecoderType = (state, action) => { 46 | switch (action.type) { 47 | case "SET_DECODER_TYPE": 48 | return action.payload; 49 | default: 50 | return state; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/static/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": { 7 | "slug": "bologna", 8 | "classes": { 9 | "Water": "#04F5EA", 10 | "Building": "#E000FF" 11 | }, 12 | "name": "Bologna, IT", 13 | "imagery": { 14 | "type": "tms", 15 | "url": "https://sitmappe.comune.bologna.it/tms/tileserver/Ortofoto2017/{z}/{x}/{y}.png" 16 | }, 17 | "encodeImages": [ 18 | ] 19 | }, 20 | "geometry": { 21 | "type": "Polygon", 22 | "coordinates": [ 23 | [ 24 | [ 25 | 11.285705566406248, 26 | 44.46992873580161 27 | ], 28 | [ 29 | 11.407241821289062, 30 | 44.46992873580161 31 | ], 32 | [ 33 | 11.407241821289062, 34 | 44.530412702249656 35 | ], 36 | [ 37 | 11.285705566406248, 38 | 44.530412702249656 39 | ], 40 | [ 41 | 11.285705566406248, 42 | 44.46992873580161 43 | ] 44 | ] 45 | ] 46 | } 47 | }, 48 | { 49 | "type": "Feature", 50 | "properties": { 51 | "slug": "farms", 52 | "classes": { 53 | "Farm": "#17FF00" 54 | }, 55 | "name": "Farms", 56 | "imagery": { 57 | "type": "tms", 58 | "url": "https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false" 59 | }, 60 | "encodeImages": [ 61 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms/39fe.json", 62 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms/6b52.json", 63 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms/7b5b.json" 64 | ] 65 | }, 66 | "geometry": { 67 | "type": "Polygon", 68 | "coordinates": [ 69 | [ 70 | [ 71 | -96.3821743, 72 | 40.5227392 73 | ], 74 | [ 75 | -96.3821738, 76 | 40.522748 77 | ], 78 | [ 79 | -96.3821721, 80 | 40.5227567 81 | ], 82 | [ 83 | -96.3821693, 84 | 40.5227652 85 | ], 86 | [ 87 | -96.3821654, 88 | 40.5227734 89 | ], 90 | [ 91 | -96.3821605, 92 | 40.5227814 93 | ], 94 | [ 95 | -96.3821545, 96 | 40.5227889 97 | ], 98 | [ 99 | -96.3821476, 100 | 40.5227959 101 | ], 102 | [ 103 | -96.3821398, 104 | 40.5228025 105 | ], 106 | [ 107 | -96.3821312, 108 | 40.5228084 109 | ], 110 | [ 111 | -96.3821218, 112 | 40.5228136 113 | ], 114 | [ 115 | -96.3821118, 116 | 40.5228182 117 | ], 118 | [ 119 | -96.3821013, 120 | 40.5228219 121 | ], 122 | [ 123 | -96.3820903, 124 | 40.5228249 125 | ], 126 | [ 127 | -96.382079, 128 | 40.5228271 129 | ], 130 | [ 131 | -96.3820674, 132 | 40.5228284 133 | ], 134 | [ 135 | -96.3820558, 136 | 40.5228289 137 | ], 138 | [ 139 | -96.370152, 140 | 40.5228642 141 | ], 142 | [ 143 | -96.3701403, 144 | 40.5228638 145 | ], 146 | [ 147 | -96.3701287, 148 | 40.5228625 149 | ], 150 | [ 151 | -96.3701174, 152 | 40.5228604 153 | ], 154 | [ 155 | -96.3701064, 156 | 40.5228575 157 | ], 158 | [ 159 | -96.3700958, 160 | 40.5228538 161 | ], 162 | [ 163 | -96.3700858, 164 | 40.5228493 165 | ], 166 | [ 167 | -96.3700764, 168 | 40.5228441 169 | ], 170 | [ 171 | -96.3700677, 172 | 40.5228383 173 | ], 174 | [ 175 | -96.3700598, 176 | 40.5228318 177 | ], 178 | [ 179 | -96.3700528, 180 | 40.5228248 181 | ], 182 | [ 183 | -96.3700467, 184 | 40.5228173 185 | ], 186 | [ 187 | -96.3700417, 188 | 40.5228094 189 | ], 190 | [ 191 | -96.3700377, 192 | 40.5228012 193 | ], 194 | [ 195 | -96.3700349, 196 | 40.5227927 197 | ], 198 | [ 199 | -96.3700331, 200 | 40.522784 201 | ], 202 | [ 203 | -96.3700325, 204 | 40.5227753 205 | ], 206 | [ 207 | -96.3699861, 208 | 40.5138478 209 | ], 210 | [ 211 | -96.3699867, 212 | 40.5138391 213 | ], 214 | [ 215 | -96.3699883, 216 | 40.5138304 217 | ], 218 | [ 219 | -96.3699911, 220 | 40.5138219 221 | ], 222 | [ 223 | -96.369995, 224 | 40.5138137 225 | ], 226 | [ 227 | -96.37, 228 | 40.5138057 229 | ], 230 | [ 231 | -96.3700059, 232 | 40.5137982 233 | ], 234 | [ 235 | -96.3700129, 236 | 40.5137911 237 | ], 238 | [ 239 | -96.3700207, 240 | 40.5137846 241 | ], 242 | [ 243 | -96.3700293, 244 | 40.5137787 245 | ], 246 | [ 247 | -96.3700386, 248 | 40.5137735 249 | ], 250 | [ 251 | -96.3700486, 252 | 40.5137689 253 | ], 254 | [ 255 | -96.3700592, 256 | 40.5137652 257 | ], 258 | [ 259 | -96.3700702, 260 | 40.5137622 261 | ], 262 | [ 263 | -96.3700815, 264 | 40.51376 265 | ], 266 | [ 267 | -96.370093, 268 | 40.5137587 269 | ], 270 | [ 271 | -96.3701047, 272 | 40.5137582 273 | ], 274 | [ 275 | -96.3820069, 276 | 40.5137229 277 | ], 278 | [ 279 | -96.3820186, 280 | 40.5137233 281 | ], 282 | [ 283 | -96.3820302, 284 | 40.5137245 285 | ], 286 | [ 287 | -96.3820415, 288 | 40.5137266 289 | ], 290 | [ 291 | -96.3820525, 292 | 40.5137296 293 | ], 294 | [ 295 | -96.3820631, 296 | 40.5137333 297 | ], 298 | [ 299 | -96.3820732, 300 | 40.5137377 301 | ], 302 | [ 303 | -96.3820826, 304 | 40.5137429 305 | ], 306 | [ 307 | -96.3820913, 308 | 40.5137488 309 | ], 310 | [ 311 | -96.3820991, 312 | 40.5137553 313 | ], 314 | [ 315 | -96.3821061, 316 | 40.5137623 317 | ], 318 | [ 319 | -96.3821122, 320 | 40.5137698 321 | ], 322 | [ 323 | -96.3821172, 324 | 40.5137777 325 | ], 326 | [ 327 | -96.3821212, 328 | 40.5137859 329 | ], 330 | [ 331 | -96.3821241, 332 | 40.5137944 333 | ], 334 | [ 335 | -96.3821258, 336 | 40.5138031 337 | ], 338 | [ 339 | -96.3821265, 340 | 40.5138118 341 | ], 342 | [ 343 | -96.3821743, 344 | 40.5227392 345 | ] 346 | ] 347 | ] 348 | } 349 | }, 350 | { 351 | "type": "Feature", 352 | "properties": { 353 | "classes": { 354 | "Road": "#FED000", 355 | "Water": "#04F5EA", 356 | "Building": "#E000FF", 357 | "Vegetation": "#17FF00" 358 | }, 359 | "slug": "s2mapstiles", 360 | "name": "Sentinel 2 - s2maps-tiles.eu", 361 | "imagery": { 362 | "type": "wms", 363 | "url": "https://s2maps-tiles.eu", 364 | "params": { 365 | "LAYERS": "s2cloudless-2021_3857" 366 | }, 367 | "serverType": "geoserver" 368 | } 369 | }, 370 | "geometry": { 371 | "type": "Polygon", 372 | "coordinates": [ 373 | [ 374 | [ 375 | -75.13830272456656, 376 | -10.836643186790951 377 | ], 378 | [ 379 | -75.13830272456656, 380 | -13.740017646606589 381 | ], 382 | [ 383 | -71.02941600581623, 384 | -13.740017646606589 385 | ], 386 | [ 387 | -71.02941600581623, 388 | -10.836643186790951 389 | ], 390 | [ 391 | -75.13830272456656, 392 | -10.836643186790951 393 | ] 394 | ] 395 | ] 396 | } 397 | }, 398 | { 399 | "type": "Feature", 400 | "properties": { 401 | "classes": { 402 | "Road": "#FED000", 403 | "Water": "#04F5EA", 404 | "Building": "#E000FF", 405 | "Vegetation": "#17FF00" 406 | }, 407 | "slug": "MODIS_Terra_CorrectedReflectance_TrueColor", 408 | "name": "MODIS Terra CR TrueColor", 409 | "imagery": { 410 | "type": "wms", 411 | "url": "https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi?", 412 | "params": { 413 | "VERSION": "1.3.0", 414 | "SERVICE": "WMS", 415 | "REQUEST": "GetMap", 416 | "FORMAT": "image/png", 417 | "STYLE": "default", 418 | "BBOX": "-180,-90,180,90", 419 | "CRS": "EPSG:4326", 420 | "HEIGHT": "600", 421 | "WIDTH": "600", 422 | "TIME": "2021-09-21", 423 | "LAYERS": "MODIS_Terra_CorrectedReflectance_TrueColor" 424 | }, 425 | "serverType": "geoserver" 426 | } 427 | }, 428 | "geometry": { 429 | "type": "Polygon", 430 | "coordinates": [ 431 | [ 432 | [ 433 | -75.13830272456656, 434 | -10.836643186790951 435 | ], 436 | [ 437 | -75.13830272456656, 438 | -13.740017646606589 439 | ], 440 | [ 441 | -71.02941600581623, 442 | -13.740017646606589 443 | ], 444 | [ 445 | -71.02941600581623, 446 | -10.836643186790951 447 | ], 448 | [ 449 | -75.13830272456656, 450 | -10.836643186790951 451 | ] 452 | ] 453 | ] 454 | } 455 | }, 456 | { 457 | "type": "Feature", 458 | "properties": { 459 | "classes": { 460 | "water": "#00ffff", 461 | "forest": "#5d9e7e", 462 | "pasture": "#00ff22", 463 | "jungle": "#248a2a", 464 | "agriculture": "#f5fb4b", 465 | "urban": "#af316d", 466 | "scrub": " #f89e49", 467 | "bare_soil": "#d1d1d1" 468 | }, 469 | "slug": "planetary_computer_lulc", 470 | "name": "Planetary Computer - LULC", 471 | "imagery": { 472 | "type": "tms", 473 | "url": "https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/82ebdc445544365e45be4db6d22536ec/{z}/{x}/{y}?assets=B04&assets=B03&assets=B02&color_formula=Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35&collection=sentinel-2-l2a" 474 | } 475 | }, 476 | "geometry": { 477 | "type": "Polygon", 478 | "coordinates": [ 479 | [ 480 | [ 481 | -102.215338, 482 | 19.584979 483 | ], 484 | [ 485 | -102.065496, 486 | 19.584979 487 | ], 488 | [ 489 | -102.065496, 490 | 19.68694 491 | ], 492 | [ 493 | -102.215338, 494 | 19.68694 495 | ], 496 | [ 497 | -102.215338, 498 | 19.584979 499 | ] 500 | ] 501 | ] 502 | } 503 | }, 504 | { 505 | "type": "Feature", 506 | "properties": { 507 | "classes": { 508 | "water": "#00ffff", 509 | "forest": "#5d9e7e", 510 | "pasture": "#00ff22", 511 | "jungle": "#248a2a", 512 | "agriculture": "#f5fb4b", 513 | "urban": "#af316d", 514 | "scrub": " #f89e49", 515 | "bare_soil": "#d1d1d1" 516 | }, 517 | "slug": "jaragua-do-sul", 518 | "name": "Jaraguá do Sul", 519 | "imagery": { 520 | "type": "tms", 521 | "url": "https://www.jaraguadosul.sc.gov.br/geo/ortomosaico2020/{z}/{x}/{y}.png" 522 | } 523 | }, 524 | "geometry": { 525 | "type": "Polygon", 526 | "coordinates": [ 527 | [ 528 | [-49.253683, -26.2656325], 529 | [-49.175491, -26.3106506], 530 | [-49.169311, -26.3580353], 531 | [-49.19403, -26.3844884], 532 | [-49.192657, -26.4201598], 533 | [-49.21051, -26.4367617], 534 | [-49.218063, -26.477334], 535 | [-49.2256164, -26.4847093], 536 | [-49.24621, -26.489011], 537 | [-49.29634, -26.541851], 538 | [-49.305953, -26.5805444], 539 | [-49.281063, -26.619531], 540 | [-49.237976, -26.6192244], 541 | [-49.20433, -26.6296596], 542 | [-49.178237, -26.616155], 543 | [-49.164505, -26.6523682], 544 | [-49.132919, -26.643162], 545 | [-49.10408, -26.61063], 546 | [-49.101333, -26.5817726], 547 | [-49.0876007, -26.5799304], 548 | [-49.0855407, -26.5516795], 549 | [-49.056701, -26.546151], 550 | [-49.051208, -26.5191209], 551 | [-49.034042, -26.5221928], 552 | [-49.017562, -26.5129767], 553 | [-49.011383, -26.482865], 554 | [-49.025115, -26.4564349], 555 | [-49.095153, -26.398635], 556 | [-49.105453, -26.3937148], 557 | [-49.1047668, -26.369724], 558 | [-49.13635, -26.3321915], 559 | [-49.138412, -26.302648], 560 | [-49.167251, -26.2657095], 561 | [-49.167251, -26.213358], 562 | [-49.19128, -26.2127429], 563 | [-49.234542, -26.2306064], 564 | [-49.2338562, -26.255241], 565 | [-49.253683, -26.2656325] 566 | ] 567 | ] 568 | } 569 | }, 570 | { 571 | "type": "Feature", 572 | "properties": { 573 | "classes": { 574 | "vessel": "#ffb350" 575 | }, 576 | "slug": "vessels", 577 | "name": "Sentinel2", 578 | "imagery": { 579 | "type": "tms", 580 | "url": "https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/82ebdc445544365e45be4db6d22536ec/{z}/{x}/{y}?assets=B04&assets=B03&assets=B02&color_formula=Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35&collection=sentinel-2-l2a" 581 | }, 582 | "encodeImages": [ 583 | "https://ds-annotate.s3.amazonaws.com/encode_images/Vessels_-_Sentinel2/1867.json", 584 | "https://ds-annotate.s3.amazonaws.com/encode_images/Vessels_-_Sentinel2/1d42.json" 585 | ] 586 | }, 587 | "geometry": { 588 | "type": "Polygon", 589 | "coordinates": [ 590 | [ 591 | [ 592 | -73.73805753373306, 593 | 40.58517186308694 594 | ], 595 | [ 596 | -73.73805753373306, 597 | 40.42698865422577 598 | ], 599 | [ 600 | -73.38991503922333, 601 | 40.42698865422577 602 | ], 603 | [ 604 | -73.38991503922333, 605 | 40.58517186308694 606 | ], 607 | [ 608 | -73.73805753373306, 609 | 40.58517186308694 610 | ] 611 | ] 612 | ] 613 | } 614 | }, 615 | { 616 | "type": "Feature", 617 | "properties": { 618 | "classes": { 619 | "Green Field": "#b9fec8", 620 | "Harvested": "#FBD900", 621 | "Bare filed": "#D52FFE" 622 | }, 623 | "slug": "Farms", 624 | "name": "Farms - Sentinel2", 625 | "imagery": { 626 | "type": "tms", 627 | "url": "https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/82ebdc445544365e45be4db6d22536ec/{z}/{x}/{y}?assets=B04&assets=B03&assets=B02&color_formula=Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35&collection=sentinel-2-l2a" 628 | }, 629 | "encodeImages": [ 630 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms_-_Sentinel2/595a.json", 631 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms_-_Sentinel2/7069.json", 632 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms_-_Sentinel2/d3e6.json", 633 | "https://ds-annotate.s3.amazonaws.com/encode_images/Farms_-_Sentinel2/f622.json" 634 | ] 635 | }, 636 | "geometry": { 637 | "type": "Polygon", 638 | "coordinates": [ 639 | [ 640 | [ 641 | -102.29232820719133, 642 | 19.063550307243275 643 | ], 644 | [ 645 | -102.29232820719133, 646 | 19.02652643034432 647 | ], 648 | [ 649 | -102.23968552362133, 650 | 19.02652643034432 651 | ], 652 | [ 653 | -102.23968552362133, 654 | 19.063550307243275 655 | ], 656 | [ 657 | -102.29232820719133, 658 | 19.063550307243275 659 | ] 660 | ] 661 | ] 662 | } 663 | }, 664 | { 665 | "type": "Feature", 666 | "properties": { 667 | "slug": "cattle", 668 | "classes": { 669 | "cattle_feeds": "#17FF00", 670 | "Pear cactus": "#ffb350", 671 | "Barley filed": "#FED000", 672 | "People": "#E222F0", 673 | "Car": "#2021EA" 674 | }, 675 | "name": "Cattle Feeds", 676 | "imagery": { 677 | "type": "tms", 678 | "url": "https://b.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.jpg?access_token=pk.eyJ1IjoicnViZW4iLCJhIjoiY202cWk1Ym15MXN0djJqcHlmNTFvN3FiYSJ9.omITaZldAEoX335pMmD3_g" 679 | } 680 | }, 681 | "geometry": { 682 | "type": "Polygon", 683 | "coordinates": [ 684 | [ 685 | [-103.33248681706525, 34.23521810170755], 686 | [-103.29645078102922, 34.23521810170755], 687 | [-103.29645078102922, 34.27125413774359], 688 | [-103.33248681706525, 34.27125413774359], 689 | [-103.33248681706525, 34.23521810170755] 690 | ] 691 | ] 692 | } 693 | } 694 | ] 695 | } -------------------------------------------------------------------------------- /src/store/indexedDB.js: -------------------------------------------------------------------------------- 1 | import { indexedDBName, indexedDBVersion } from "./../config"; 2 | let db; 3 | 4 | export const openDatabase = () => { 5 | return new Promise((resolve, reject) => { 6 | const request = indexedDB.open(indexedDBName, indexedDBVersion); 7 | 8 | request.onupgradeneeded = (event) => { 9 | db = event.target.result; 10 | //Items 11 | const objectStoreItems = db.createObjectStore("items", { keyPath: "id" }); 12 | objectStoreItems.createIndex("project", "project", { unique: false }); 13 | //Encode images Items 14 | const objectStoreEncodeItems = db.createObjectStore("encodeItems", { 15 | keyPath: "id", 16 | }); 17 | objectStoreEncodeItems.createIndex("project", "project", { 18 | unique: false, 19 | }); 20 | }; 21 | 22 | request.onsuccess = (event) => { 23 | db = event.target.result; 24 | resolve(); 25 | }; 26 | 27 | request.onerror = (event) => { 28 | reject("Database error: ", event.target.error); 29 | }; 30 | }); 31 | }; 32 | 33 | class Store { 34 | constructor(storeName) { 35 | this.storeName = storeName; 36 | } 37 | 38 | transaction(mode) { 39 | return db.transaction(this.storeName, mode).objectStore(this.storeName); 40 | } 41 | 42 | addData(item) { 43 | return new Promise((resolve, reject) => { 44 | const request = this.transaction("readwrite").add({ 45 | id: item.id || item.properties.id, 46 | project: item.project || item.properties.project, 47 | ...item, 48 | }); 49 | request.onsuccess = () => resolve(request.result); 50 | request.onerror = () => reject(request.error); 51 | }); 52 | } 53 | 54 | getAllData() { 55 | return new Promise((resolve, reject) => { 56 | const request = this.transaction("readonly").getAll(); 57 | request.onsuccess = () => resolve(request.result); 58 | request.onerror = () => reject(request.error); 59 | }); 60 | } 61 | 62 | getDataByProject(project) { 63 | return new Promise((resolve, reject) => { 64 | const store = this.transaction("readonly"); 65 | const index = store.index("project"); 66 | const request = index.getAll(IDBKeyRange.only(project)); 67 | request.onsuccess = () => resolve(request.result); 68 | request.onerror = () => reject(request.error); 69 | }); 70 | } 71 | 72 | deleteData(id) { 73 | return new Promise((resolve, reject) => { 74 | const request = this.transaction("readwrite").delete(id); 75 | request.onsuccess = () => resolve(request.result); 76 | request.onerror = () => reject(request.error); 77 | }); 78 | } 79 | 80 | deleteDataByProject(project) { 81 | return new Promise((resolve, reject) => { 82 | const store = this.transaction("readwrite"); 83 | const index = store.index("project"); 84 | const request = index.openCursor(IDBKeyRange.only(project)); 85 | request.onsuccess = () => { 86 | const cursor = request.result; 87 | if (cursor) { 88 | store.delete(cursor.primaryKey); 89 | cursor.continue(); 90 | } else { 91 | resolve(); 92 | } 93 | }; 94 | request.onerror = () => reject(request.error); 95 | }); 96 | } 97 | 98 | deleteAllData() { 99 | return new Promise((resolve, reject) => { 100 | const request = this.transaction("readwrite").clear(); 101 | request.onsuccess = () => resolve(request.result); 102 | request.onerror = () => reject(request.error); 103 | }); 104 | } 105 | 106 | // addListData(list) { 107 | // return new Promise((resolve, reject) => { 108 | // const store = this.transaction("readwrite"); 109 | // list.forEach((item) => store.add({ id: item.properties.id, ...item })); 110 | // store.transaction.oncomplete = () => resolve(); 111 | // store.transaction.onerror = () => reject(store.transaction.error); 112 | // }); 113 | // } 114 | } 115 | 116 | export const storeItems = new Store("items"); 117 | export const storeEncodeItems = new Store("encodeItems"); 118 | -------------------------------------------------------------------------------- /src/utils/calculation.js: -------------------------------------------------------------------------------- 1 | import * as turf from "@turf/turf"; 2 | 3 | export function lngLatToPixel(flatCoordinates, bbox, image_shape) { 4 | // Extract the bounding box coordinates 5 | const [minLng, minLat, maxLng, maxLat] = bbox; 6 | const [mapHeight, mapWidth] = image_shape; 7 | const [lng, lat] = flatCoordinates; 8 | // Calculate the percentage of the point's position in the bounding box 9 | const xFactor = (lng - minLng) / (maxLng - minLng); 10 | const yFactor = (lat - minLat) / (maxLat - minLat); 11 | // Convert the percentage to pixel position 12 | const x = xFactor * mapWidth; 13 | const y = (1 - yFactor) * mapHeight; 14 | return { 15 | y: Math.round(y), 16 | x: Math.round(x), 17 | }; 18 | } 19 | 20 | /** 21 | * Check Clicked point falls into the bbox og the encode cached images 22 | * @param {*} map 23 | * @param {*} pointsSelector 24 | * @returns objects of properties for decode request 25 | */ 26 | export const pointIsInEncodeBbox = (encodeItem, pointSelector) => { 27 | // select the first point 28 | const pointFeature = pointSelector; 29 | const geometry = pointFeature.getGeometry(); 30 | const flatCoordinates = geometry.getFlatCoordinates(); 31 | 32 | //point 33 | const pt = turf.point(flatCoordinates); 34 | // polygon 35 | const { bbox, image_shape } = encodeItem; 36 | let polygonCoords = [ 37 | [ 38 | [bbox[0], bbox[1]], 39 | [bbox[0], bbox[3]], 40 | [bbox[2], bbox[3]], 41 | [bbox[2], bbox[1]], 42 | [bbox[0], bbox[1]], 43 | ], 44 | ]; 45 | 46 | const poly = turf.polygon(polygonCoords); 47 | const isPointInPolygon = turf.booleanPointInPolygon(pt, poly); 48 | let pixels = {}; 49 | if (isPointInPolygon) { 50 | pixels = lngLatToPixel(flatCoordinates, bbox, image_shape); 51 | } 52 | return pixels; 53 | }; 54 | 55 | export const pointsIsInEncodeBbox = (encodeItem, pointsSelector) => { 56 | const listPixels = pointsSelector 57 | .map(function (pointSelector) { 58 | const pixel = pointIsInEncodeBbox(encodeItem, pointSelector); 59 | if (Object.keys(pixel).length > 0) { 60 | return [pixel.x, pixel.y]; 61 | } 62 | }) 63 | .filter(function (pixel) { 64 | return pixel !== undefined; 65 | }); 66 | return listPixels; 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/canvas.js: -------------------------------------------------------------------------------- 1 | import Tile from "ol/layer/Tile"; 2 | import Map from "ol/Map"; 3 | import cloneDeep from "lodash/cloneDeep"; 4 | 5 | export const getCanvas = (map) => { 6 | if (!map) return; 7 | const mapCanvas = document.createElement("canvas"); 8 | const size = map.getSize(); 9 | mapCanvas.width = size[0]; 10 | mapCanvas.height = size[1]; 11 | const mapContext = mapCanvas.getContext("2d"); 12 | Array.prototype.forEach.call( 13 | map.getViewport().querySelectorAll(".ol-layer canvas, canvas.ol-layer"), 14 | function (canvas) { 15 | if (canvas.width > 0) { 16 | const opacity = canvas.parentNode.style.opacity || canvas.style.opacity; 17 | mapContext.globalAlpha = opacity === "" ? 1 : Number(opacity); 18 | let matrix; 19 | const transform = canvas.style.transform; 20 | if (transform) { 21 | // Get the transform parameters from the style's transform matrix 22 | matrix = transform 23 | .match(/^matrix\(([^\(]*)\)$/)[1] 24 | .split(",") 25 | .map(Number); 26 | } else { 27 | matrix = [ 28 | parseFloat(canvas.style.width) / canvas.width, 29 | 0, 30 | 0, 31 | parseFloat(canvas.style.height) / canvas.height, 32 | 0, 33 | 0, 34 | ]; 35 | } 36 | // Apply the transform to the export map context 37 | CanvasRenderingContext2D.prototype.setTransform.apply( 38 | mapContext, 39 | matrix 40 | ); 41 | const backgroundColor = canvas.parentNode.style.backgroundColor; 42 | if (backgroundColor) { 43 | mapContext.fillStyle = backgroundColor; 44 | mapContext.fillRect(0, 0, canvas.width, canvas.height); 45 | } 46 | mapContext.drawImage(canvas, 0, 0); 47 | } 48 | } 49 | ); 50 | mapContext.globalAlpha = 1; 51 | mapContext.setTransform(1, 0, 0, 1, 0, 0); 52 | const canvas = mapCanvas.toDataURL("image/jpeg", 0.9); 53 | return canvas; 54 | }; 55 | 56 | export const getCanvasForLayer = (map, layerTitle) => { 57 | if (!map) return; 58 | const size = map.getSize(); 59 | var layerGroup = map.getLayerGroup(); 60 | var myLayer = null; 61 | layerGroup.getLayers().forEach(function (layer) { 62 | if (layer && layer.get("title") === layerTitle) { 63 | myLayer = layer; 64 | } 65 | }); 66 | if (myLayer && myLayer instanceof Tile) { 67 | const div = document.createElement("div"); 68 | div.setAttribute( 69 | "style", 70 | `position: absolute; visibility: hidden; height: ${size[1]}px; width: ${size[0]}px; background: #456299;` 71 | ); 72 | div.setAttribute("id", "map"); 73 | document.body.appendChild(div); 74 | const clonedMap = new Map({ 75 | target: "map", 76 | layers: [cloneDeep(myLayer)], 77 | view: cloneDeep(map.getView()), 78 | }); 79 | // Set some time to load the map 80 | return new Promise((resolve) => 81 | setTimeout(function () { 82 | const canvas = getCanvas(clonedMap); 83 | clonedMap.dispose(); 84 | div.remove(); 85 | resolve(canvas); 86 | }, 3000) 87 | ); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/utils/convert.js: -------------------------------------------------------------------------------- 1 | import GeoJSON from "ol/format/GeoJSON"; 2 | import * as turf from "@turf/turf"; 3 | // import { proj } from 'ol/proj'; 4 | import { transformExtent } from "ol/proj"; 5 | import { guid } from "./utils"; 6 | 7 | /** 8 | * 9 | * @param {Array} geojsonFeature 10 | * @returns {Array} Array of openLayer features 11 | */ 12 | export const geojson2olFeatures = (geojsonFeature) => { 13 | const fc = turf.featureCollection([geojsonFeature]); 14 | const oLFeatures = new GeoJSON().readFeatures(fc, { 15 | featureProjection: "EPSG:3857", 16 | dataProjection: "EPSG:4326", 17 | }); 18 | return oLFeatures; 19 | }; 20 | 21 | export const getOLFeatures = (features) => { 22 | const fc = turf.featureCollection(features); 23 | const oLFeatures = new GeoJSON().readFeatures(fc); 24 | return oLFeatures; 25 | }; 26 | 27 | /** 28 | * Convert list of features to OpenLayers features 29 | * @param {Array} array of feature 30 | * @returns {Array} Array of openLayer features 31 | */ 32 | export const features2olFeatures = (features) => { 33 | const fc = turf.featureCollection(features); 34 | const oLFeatures = new GeoJSON().readFeatures(fc, { 35 | featureProjection: "EPSG:3857", 36 | dataProjection: "EPSG:4326", 37 | }); 38 | return oLFeatures; 39 | }; 40 | 41 | /** 42 | * 43 | * @param {Array[olFeatures]} olFeatures 44 | * @returns {List} list of features 45 | */ 46 | export const olFeatures2Features = (olFeatures) => { 47 | var geojson = new GeoJSON().writeFeatures(olFeatures, { 48 | dataProjection: "EPSG:4326", 49 | featureProjection: "EPSG:3857", 50 | properties: ["px", "py"], 51 | }); 52 | return JSON.parse(geojson).features; 53 | }; 54 | 55 | /** 56 | * 57 | * @param {Array[olFeatures]} olFeatures 58 | * @returns {Object} GeoJson object 59 | */ 60 | export const olFeatures2geojson = (olFeatures) => { 61 | var geojson = new GeoJSON().writeFeatures(olFeatures, { 62 | dataProjection: "EPSG:4326", 63 | featureProjection: "EPSG:3857", 64 | properties: ["px", "py"], 65 | }); 66 | return JSON.parse(geojson); 67 | }; 68 | 69 | export const convertBbox3857to4326 = (bbox) => { 70 | const convertedBbox = transformExtent(bbox, "EPSG:3857", "EPSG:4326"); 71 | return convertedBbox; 72 | }; 73 | 74 | export const convertBbox4326to3857 = (bbox) => { 75 | const convertedBbox = transformExtent(bbox, "EPSG:4326", "EPSG:3857"); 76 | return convertedBbox; 77 | }; 78 | 79 | /** 80 | * 81 | * @param {Object} feature of project 82 | * @returns {Array} List of objects of classes 83 | */ 84 | export const getClassLayers = (project) => { 85 | return Object.entries( 86 | project && project.properties ? project.properties.classes : {} 87 | ).map((e) => ({ 88 | name: e[0], 89 | color: e[1], 90 | })); 91 | }; 92 | 93 | /** 94 | * 95 | * @param {Object} feature of project 96 | * @returns {Array} List of objects of classes 97 | */ 98 | export const sam2Geojson = (ListGeoms, activeProject, activeClass, id) => { 99 | let scores = []; 100 | const features = []; 101 | for (let index = 0; index < ListGeoms.length; index++) { 102 | const strGeom = ListGeoms[index]; 103 | const geom = JSON.parse(strGeom); 104 | const properties = { 105 | class: activeClass.name, 106 | color: activeClass.color, 107 | project: activeProject.properties.name, 108 | ...geom.properties, 109 | id: id, 110 | }; 111 | scores = geom.properties.confidence_scores; 112 | const feature = turf.multiPolygon(geom.coordinates, properties); 113 | features.push(feature); 114 | } 115 | const maxNumber = Math.max(...scores); 116 | const maxIndex = scores.indexOf(maxNumber); 117 | const maxScoreFeature = features[maxIndex]; 118 | return [maxScoreFeature]; 119 | // return features 120 | }; 121 | 122 | export const setProps2Features = (features, activeProject, activeClass, id) => { 123 | const properties = { 124 | class: activeClass.name, 125 | color: activeClass.color, 126 | project: activeProject.properties.name, 127 | }; 128 | 129 | const newId = guid(); 130 | return features.map((feature, index) => { 131 | return { 132 | ...feature, 133 | id: `${newId}_${index}`, 134 | properties: { 135 | ...feature.properties, 136 | ...properties, 137 | id: `${newId}_${index}`, 138 | }, 139 | }; 140 | }); 141 | }; 142 | 143 | export const bbox2polygon = (bbox) => { 144 | const poly = turf.bboxPolygon(bbox); 145 | return poly; 146 | }; 147 | 148 | export const saveFeaturesToGeoJSONFile = (features) => { 149 | const geoJSON = { 150 | type: "FeatureCollection", 151 | features: features, 152 | }; 153 | 154 | const geoJSONString = JSON.stringify(geoJSON, null, 2); 155 | 156 | const blob = new Blob([geoJSONString], { type: "application/geo+json" }); 157 | 158 | const url = URL.createObjectURL(blob); 159 | 160 | const link = document.createElement("a"); 161 | link.href = url; 162 | link.download = `results.geojson`; 163 | 164 | document.body.appendChild(link); 165 | link.click(); 166 | 167 | document.body.removeChild(link); 168 | 169 | URL.revokeObjectURL(url); 170 | }; 171 | -------------------------------------------------------------------------------- /src/utils/notifications.js: -------------------------------------------------------------------------------- 1 | import { NotificationManager } from "react-notifications"; 2 | 3 | export const setDefaultNotification = (text, title, duration) => { 4 | NotificationManager.warning(text, title, duration); 5 | }; 6 | 7 | // export const setCustomNotification = (text, title, duration) => { 8 | // if (!notificationShown) { 9 | // NotificationManager.warning(text, title, duration); 10 | // setNotificationShown(true); 11 | // setTimeout(() => { 12 | // setNotificationShown(false); 13 | // }, 10 * 1000); 14 | // } 15 | // }; 16 | -------------------------------------------------------------------------------- /src/utils/requests.js: -------------------------------------------------------------------------------- 1 | import { SamGeoAPI } from "../config"; 2 | import { NotificationManager } from "react-notifications"; 3 | import { olFeatures2geojson } from "./convert"; 4 | import { convertBbox3857to4326 } from "./convert"; 5 | 6 | const headers = { 7 | Accept: "application/json", 8 | "Content-Type": "application/json", 9 | }; 10 | 11 | /** 12 | * 13 | * @param {*} map 14 | * @param {*} pointsSelector 15 | * @returns objects of properties for decode request 16 | */ 17 | export const getPropertiesRequest = (map, pointsSelector) => { 18 | const fcPoints = olFeatures2geojson(pointsSelector); 19 | const coords = fcPoints.features.map((f) => [ 20 | f.properties.px, 21 | f.properties.py, 22 | ]); 23 | const [imgWidth, imgHeight] = map.getSize(); 24 | // Get the view CRS and extent 25 | const view = map.getView(); 26 | const zoom = view.getZoom(); 27 | const projection = view.getProjection(); 28 | const crs = projection.getCode(); 29 | const bbox = view.calculateExtent(map.getSize()); 30 | const reqProps = { 31 | image_shape: [imgHeight, imgWidth], 32 | input_label: 1, 33 | input_point: coords[0], 34 | crs, 35 | bbox, 36 | zoom, 37 | }; 38 | return reqProps; 39 | }; 40 | 41 | // SAM2 42 | export const getRequest = async (url) => { 43 | const reqUrl = `${SamGeoAPI}/${url}`; 44 | try { 45 | const response = await fetch(reqUrl); 46 | if (!response.ok) { 47 | throw new Error(`Error fetching data: ${response.status}`); 48 | } 49 | const result = await response.json(); 50 | return result; 51 | } catch (error) { 52 | console.error("Error fetching GeoJSON data:", error); 53 | return null; 54 | } 55 | }; 56 | 57 | // SAM2 58 | export const setAOI = async (encodeItem) => { 59 | const url = `${SamGeoAPI}/aoi`; 60 | try { 61 | const reqProps = { 62 | canvas_image: encodeItem.canvas, 63 | bbox: convertBbox3857to4326(encodeItem.bbox), 64 | zoom: Math.floor(encodeItem.zoom), 65 | crs: "EPSG:4326", 66 | id: encodeItem.id, 67 | project: encodeItem.project, 68 | }; 69 | const encodeResponse = await fetch(url, { 70 | method: "POST", 71 | headers, 72 | body: JSON.stringify(reqProps), 73 | }); 74 | if (!encodeResponse.ok) { 75 | NotificationManager.error( 76 | `${url}`, 77 | `Encode server error ${encodeResponse.status}`, 78 | 10000 79 | ); 80 | throw new Error(`Error: ${encodeResponse.status}`); 81 | } 82 | const encodeRespJson = await encodeResponse.json(); 83 | return encodeRespJson; 84 | } catch (error) { 85 | console.log(error); 86 | } 87 | }; 88 | 89 | // SAM2 90 | export const requestSegments = async (payload, url_path) => { 91 | const apiUrl = `${SamGeoAPI}/${url_path}`; 92 | try { 93 | // Decode 94 | const resp = await fetch(apiUrl, { 95 | method: "POST", 96 | headers, 97 | body: JSON.stringify(payload), 98 | }); 99 | 100 | if (!resp.ok) { 101 | NotificationManager.error( 102 | `${apiUrl} `, 103 | `Decode server error ${resp.status}`, 104 | 10000 105 | ); 106 | throw new Error(`Error: ${resp.status}`); 107 | } 108 | const decodeRespJson = await resp.json(); 109 | return decodeRespJson; 110 | } catch (error) { 111 | console.log(error); 112 | } 113 | }; 114 | 115 | export const requestEncodeImages = async (project_id) => { 116 | const apiUrl = `${SamGeoAPI}/predictions?project_id=${project_id}`; 117 | try { 118 | const resp = await fetch(apiUrl, { 119 | method: "GET", 120 | headers, 121 | }); 122 | const decodeRespJson = await resp.json(); 123 | // TODO, change here to handle response 124 | if (decodeRespJson.detail) { 125 | decodeRespJson.detection = {}; 126 | return decodeRespJson; 127 | } 128 | return decodeRespJson; 129 | } catch (error) { 130 | console.log(error); 131 | } 132 | }; 133 | 134 | export const fetchGeoJSONData = async (propsReq) => { 135 | const url = 136 | "https://gist.githubusercontent.com/Rub21/c7001da2925661a4e660fde237e94473/raw/88f6f163029188dd1c8e3c23ff66aaaa8a6bac93/sam2_result.json"; 137 | try { 138 | const response = await fetch(url); 139 | if (!response.ok) { 140 | throw new Error(`Error fetching data: ${response.status}`); 141 | } 142 | const data = await response.json(); 143 | return { geojson: data }; 144 | } catch (error) { 145 | console.error("Error fetching GeoJSON data:", error); 146 | } 147 | }; 148 | 149 | /** 150 | * 151 | * @param {list} urls 152 | * @returns list of response json 153 | */ 154 | export const fetchListURLS = async (urls) => { 155 | const fetchPromises = urls.map(async (url) => { 156 | try { 157 | const response = await fetch(url); 158 | if (!response.ok) { 159 | console.error(`HTTP error! status: ${response.status}`); 160 | return undefined; 161 | } 162 | const data = await response.json(); 163 | return data; 164 | } catch (error) { 165 | console.error("Error fetching data from URL: ", url, " Error: ", error); 166 | return undefined; 167 | } 168 | }); 169 | 170 | let results = await Promise.all(fetchPromises); 171 | results = results.filter((r) => r !== undefined); 172 | return results; 173 | }; 174 | 175 | /** 176 | * Download data in JOSM 177 | * @param {*} data 178 | * @param {*} project 179 | */ 180 | export const downloadInJOSM = (data, project, id) => { 181 | const url = `${SamGeoAPI}/upload_geojson`; 182 | fetch(url, { 183 | method: "POST", 184 | headers, 185 | body: JSON.stringify({ data, project: project.properties.slug, id }), 186 | }) 187 | .then((response) => { 188 | return response.json(); 189 | }) 190 | .then((data) => { 191 | const { url, type } = project.properties.imagery; 192 | const layer_name = project.properties.name.replace(/ /g, "_"); 193 | const url_geojson = `http://localhost:8111/import?url=${data.file_url.replace( 194 | "https", 195 | "http" 196 | )}`; 197 | fetch(url_geojson); 198 | const url_layer = `​http://localhost:8111/imagery?title=${layer_name}&type=${type}&url=${url}`; 199 | fetch(url_layer); 200 | }); 201 | }; 202 | -------------------------------------------------------------------------------- /src/utils/tests/featureCollection.test.js: -------------------------------------------------------------------------------- 1 | import { olFeatures2geojson, geojson2olFeatures } from "../convert"; 2 | import { FEATURES_3857, FEATURES_4326 } from "./fixtures"; 3 | import GeoJSON from "ol/format/GeoJSON"; 4 | import VectorSource from "ol/source/Vector"; 5 | 6 | describe("Convert OpenLayer features to Geojson", () => { 7 | const vectorSource = new VectorSource({ 8 | features: new GeoJSON().readFeatures(FEATURES_3857), 9 | }); 10 | //EPSG:3857: [-5e6, -1e6] = EPSG:4326: [-44.91576420597607, -8.946573850543416] 11 | const olCoordinates = vectorSource 12 | .getFeatures()[0] 13 | .getGeometry() 14 | .getCoordinates(); 15 | const geojson = olFeatures2geojson(vectorSource.getFeatures()); 16 | const vCoordinates = geojson.features[0].geometry.coordinates[0][0]; 17 | test("should return coordinates in EPSG:4326", () => { 18 | expect(olCoordinates[0][0]).toEqual([-5000000, -1000000]); 19 | expect(vCoordinates).toEqual([-44.91576420597607, -8.946573850543416]); 20 | }); 21 | }); 22 | 23 | describe("Convert geojson to OpenLayer features", () => { 24 | const vFeature = FEATURES_4326.features[0]; 25 | const vCoordinates = vFeature.geometry.coordinates[0][0]; 26 | const olFeature = geojson2olFeatures(vFeature)[0]; 27 | const olCoordinates = olFeature.getGeometry().getCoordinates()[0][0]; 28 | test("should return coordinates in EPSG:3857", () => { 29 | expect(vCoordinates).toEqual([-74.24398520109803, -13.133541195879786]); 30 | expect(olCoordinates).toEqual([-8264802.627049572, -1474993.15984846]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/utils/tests/transformation.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | simplifyMultipolygon, 3 | simplifyOlFeature, 4 | unionPolygons, 5 | } from "../transformation"; 6 | import { geojson2olFeatures } from "../convert"; 7 | 8 | import { FEATURES_4326 } from "./fixtures"; 9 | 10 | describe("Merge polygons", () => { 11 | test("should return 3 mumber of features", () => { 12 | expect(unionPolygons(FEATURES_4326.features).length).toBe(3); 13 | }); 14 | }); 15 | 16 | describe("Simplify Multipolygon", () => { 17 | test("should return 4 mumber of features", () => { 18 | expect(simplifyMultipolygon(FEATURES_4326.features).length).toBe(4); 19 | }); 20 | }); 21 | 22 | describe("Simplify Open Layer feature", () => { 23 | const oLFeature = geojson2olFeatures(FEATURES_4326.features[3])[0]; 24 | const oLFeatureCoords = oLFeature.getGeometry().getCoordinates(); 25 | const newOlFeatureCoords = simplifyOlFeature(oLFeature, 0.00000001) 26 | .getGeometry() 27 | .getCoordinates(); 28 | test("should return Openlayer feature-coordinates greater than then simplified OL feature", () => { 29 | expect(oLFeatureCoords[0].length).toEqual(newOlFeatureCoords[0].length); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/utils/tests/util.test.js: -------------------------------------------------------------------------------- 1 | import { getProjectTemplate } from "../utils"; 2 | describe("Check query params", () => { 3 | const url_ = 4 | "http://localhost:3000/ds-annotate?classes=farm,00FFFF|tree,FF00FF&project=Farms-mapping&imagery_type=tms&imagery_url=https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false&project_bbox=-90.319317,38.482965,-90.247220,38.507418"; 5 | let url = new URL(url_); 6 | let params = new URLSearchParams(url.search); 7 | const projectFeature = getProjectTemplate(params); 8 | 9 | test(`Should return project=${params.get("project")}`, () => { 10 | expect(projectFeature.properties.name).toBe(params.get("project")); 11 | }); 12 | 13 | test(`Should return bbox=${params.get("project_bbox")}`, () => { 14 | expect(projectFeature.bbox).toEqual([ 15 | -90.319317, 38.482965, -90.24722, 38.507418, 16 | ]); 17 | }); 18 | 19 | test("Should return properties", () => { 20 | expect(projectFeature.properties).toEqual({ 21 | slug: "Farms-mapping", 22 | name: "Farms-mapping", 23 | classes: { farm: "#00FFFF", tree: "#FF00FF" }, 24 | imagery: { 25 | type: "tms", 26 | url: "https://gis.apfo.usda.gov/arcgis/rest/services/NAIP/USDA_CONUS_PRIME/ImageServer/tile/{z}/{y}/{x}?blankTile=false", 27 | }, 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/utils/transformation.js: -------------------------------------------------------------------------------- 1 | import * as turf from "@turf/turf"; 2 | import { geojson2olFeatures, olFeatures2geojson } from "./convert"; 3 | import { guid } from "./utils"; 4 | 5 | /** 6 | * Simplify multipolygon, to do that, We get the area from the out polygon area and 7 | * inner polygon, if the inere polygon is less than 20% of area of our polygon, 8 | * it should be removed from the multipolygon. 9 | * @param {*} features 10 | * @returns 11 | */ 12 | export const simplifyMultipolygon = (features) => { 13 | let new_features = []; 14 | for (let index = 0; index < features.length; index++) { 15 | const feature = features[index]; 16 | let global_area = turf.area(feature); 17 | let coordinates = feature.geometry.coordinates; 18 | let new_coords = []; 19 | for (let j = 0; j < coordinates.length; j++) { 20 | const coord = coordinates[j]; 21 | if (coord.length >= 4) { 22 | let area = turf.area(turf.polygon([coord])); 23 | if (area > global_area * 0.2) { 24 | new_coords.push(coord); 25 | } 26 | } 27 | } 28 | feature.geometry.coordinates = new_coords; 29 | new_features.push(feature); 30 | } 31 | return new_features; 32 | }; 33 | 34 | /** 35 | * Smooth features 36 | * @param {*} features 37 | * @returns 38 | */ 39 | export const smooth = (features) => { 40 | let smoothed = turf.polygonSmooth(turf.featureCollection(features), { 41 | iterations: 2, 42 | }); 43 | const newFeatures = smoothed.features.map((fea, index) => { 44 | const id = `${guid()}_${index}`; 45 | fea.id = id; 46 | fea.properties.id = id; 47 | return fea; 48 | }); 49 | return newFeatures; 50 | }; 51 | 52 | /** 53 | * Simplify features according to tolerance 54 | * @param {Array} features 55 | * @param {number} tolerance 56 | * @returns 57 | */ 58 | export const simplifyFeatures = (features, tolerance) => { 59 | let options = { tolerance: tolerance, highQuality: true }; 60 | let simplified = turf.simplify(turf.featureCollection(features), options); 61 | return simplified.features; 62 | }; 63 | 64 | /** 65 | * Simplify a OpenLayer Feature 66 | * @param {*} olFeature 67 | * @returns 68 | */ 69 | export const simplifyOlFeature = (olFeature, tolerance) => { 70 | const feature = olFeatures2geojson([olFeature]).features[0]; 71 | let geojsonFeature = simplifyFeatures( 72 | // smooth(simplifyMultipolygon([feature])), 73 | simplifyMultipolygon([feature]), 74 | tolerance 75 | )[0]; 76 | // let geojsonFeature = simplifyMultipolygon([feature])[0]; 77 | // new_features.map((f) => (f.properties.color = '#0000FF')); 78 | geojsonFeature.properties = feature.properties; 79 | const newOLFeature = geojson2olFeatures(geojsonFeature)[0]; 80 | return newOLFeature; 81 | }; 82 | 83 | /** 84 | * Merge polygon by class 85 | * @param {Object} Geojson features 86 | */ 87 | 88 | export const mergePolygonClass = (features) => { 89 | const grouped = features.reduce((result, current) => { 90 | const category = current.properties.class; 91 | 92 | if (!result[category]) { 93 | result[category] = []; 94 | } 95 | result[category].push(current); 96 | return result; 97 | }, {}); 98 | 99 | let results = []; 100 | for (const class_ in grouped) { 101 | results = results.concat(unionPolygons(grouped[class_])); 102 | } 103 | return results; 104 | }; 105 | 106 | /** 107 | * Merge polygons 108 | * @param {Object} Geojson features 109 | * @returns 110 | */ 111 | export const unionPolygons = (features) => { 112 | let result = null; 113 | let props = {}; 114 | 115 | features.forEach(function (feature) { 116 | if (!result) { 117 | result = feature; 118 | props = feature.properties; 119 | } else { 120 | result = turf.union(result, feature); 121 | } 122 | }); 123 | 124 | let new_features = []; 125 | if (result && result.geometry && result.geometry.type === "MultiPolygon") { 126 | new_features = result.geometry.coordinates.map((coords, index) => { 127 | const newId = `${guid()}_${index}`; 128 | const singlePolygon = turf.polygon(coords, { ...props, id: newId }); 129 | singlePolygon.id = newId; 130 | return singlePolygon; 131 | }); 132 | } else if (result && result.geometry && result.geometry.type === "Polygon") { 133 | result.properties = props; 134 | result.id = props.id; 135 | new_features = [result]; 136 | } else { 137 | new_features = features; 138 | } 139 | return new_features; 140 | }; 141 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import * as turf from "@turf/turf"; 2 | 3 | /** 4 | * Download file 5 | * @param {object} data 6 | * @param {string} fileName 7 | */ 8 | export const downloadJsonFile = (data, fileName) => { 9 | const a = document.createElement("a"); 10 | document.body.appendChild(a); 11 | a.style = "display: none"; 12 | const blob = new Blob([data], { type: "octet/stream" }); 13 | const url = window.URL.createObjectURL(blob); 14 | a.href = url; 15 | a.download = fileName; 16 | a.click(); 17 | window.URL.revokeObjectURL(url); 18 | }; 19 | 20 | export const getProjectTemplate = (searchParams) => { 21 | // Set project from Query 22 | const classes_items = searchParams.get("classes"); 23 | const project = searchParams.get("project"); 24 | const imagery_type = searchParams.get("imagery_type"); 25 | const imagery_url = searchParams.get("imagery_url"); 26 | let project_bbox = searchParams.get("project_bbox"); 27 | let project_geometry = searchParams.get("project_geometry"); 28 | let projectFeature = null; 29 | if ( 30 | classes_items && 31 | project && 32 | imagery_type && 33 | imagery_url && 34 | (project_bbox || project_geometry) 35 | ) { 36 | if (project_bbox) { 37 | project_bbox = project_bbox.split(",").map((i) => Number(i)); 38 | projectFeature = turf.bboxPolygon(project_bbox); 39 | } else if (project_geometry) { 40 | projectFeature = turf.polygon(JSON.parse(project_geometry)); 41 | } 42 | projectFeature.properties.slug = project; 43 | projectFeature.properties.name = project; 44 | projectFeature.properties.classes = {}; 45 | classes_items.split("|").forEach((item) => { 46 | const tuple = item.split(","); 47 | projectFeature.properties.classes[tuple[0]] = `#${tuple[1]}`; 48 | }); 49 | 50 | projectFeature.properties.imagery = { 51 | type: imagery_type, 52 | url: imagery_url, 53 | }; 54 | } 55 | return projectFeature; 56 | }; 57 | 58 | /** 59 | * convert color code to RGBA 60 | * @param {string} colorCode 61 | * @param {float} opacity 62 | * @returns 63 | */ 64 | export const convertColorToRGBA = (colorCode, opacity) => { 65 | colorCode = colorCode.replace("#", ""); 66 | const red = parseInt(colorCode.substring(0, 2), 16); 67 | const green = parseInt(colorCode.substring(2, 4), 16); 68 | const blue = parseInt(colorCode.substring(4, 6), 16); 69 | const rgba = `rgba(${red}, ${green}, ${blue}, ${opacity})`; 70 | return rgba; 71 | }; 72 | 73 | /** 74 | * 75 | * @returns Strign of 4 characters 76 | */ 77 | export const guid = () => { 78 | var w = () => { 79 | return Math.floor((1 + Math.random()) * 0x10000) 80 | .toString(16) 81 | .substring(1); 82 | }; 83 | return `${w().substring(0, 3)}`; 84 | }; 85 | 86 | /** 87 | * replace empty space with _ 88 | * @param {String} name 89 | * @returns 90 | */ 91 | export const simplyName = (name) => { 92 | return name.replace(/\s/g, "_"); 93 | }; 94 | 95 | export const getFileNameFromURL = (url) => { 96 | const urlObject = new URL(url); 97 | const pathname = urlObject.pathname; 98 | const filenameWithExtension = pathname.split("/").pop(); 99 | const filename = filenameWithExtension.split(".").slice(0, -1).join("."); 100 | return filename; 101 | }; 102 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: { 7 | colors:{ 8 | "dark": "#081a51", 9 | "light-white": "rbga(255,255,255,0.18)", 10 | "orange-ds": "#CF3F02", 11 | }, 12 | fontSize: { 13 | 'xxs': '0.625rem', 14 | }, 15 | fontFamily: { 16 | sans: ['Open Sans', 'sans-serif'], 17 | }, 18 | }, 19 | }, 20 | plugins: [], 21 | } --------------------------------------------------------------------------------