├── .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 | 
6 |
7 |
8 |
9 | **This application consumes a service API from [samgeo-service](https://github.com/GeoCompas/samgeo-service).**
10 |
11 |
12 |
13 | Your browser does not support the video tag.
14 |
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 | You need to enable JavaScript to run this app.
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 |
39 | {classes.map((classProps, index) => (
40 |
41 | ))}
42 |
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 |
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 | requestAutomatic("automatic")}
84 | >
85 | {`Detect objects automatic`}
86 |
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 |
162 | tagChangeHandler("single_point")
163 | }
164 | >
165 | Single
166 |
167 | tagChangeHandler("multi_point")}
174 | >
175 | Multi
176 |
177 |
178 |
179 |
180 | {/* Tab Content based on Selected Tab */}
181 |
182 | {selectedTab === "singlePolygon" && (
183 | <>
184 |
185 | {/* Foreground and Background Buttons */}
186 |
187 | setIsForegroundPromtPoint(true)}
191 | >
192 | Foreground
193 |
194 | setIsForegroundPromtPoint(false)}
198 | >
199 | Background
200 |
201 |
202 |
requestPointPromt("single_point")}
205 | >
206 | Detect Single Polygon
207 |
208 | >
209 |
210 | )}
211 | {selectedTab === "multiPolygon" && (
212 |
requestPointPromt("multi_point")}
215 | >
216 | Detect Multi Polygon
217 |
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 | requestAOI()}
64 | >
65 | New SAM AOI
66 |
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 |
64 | Import SAM AOIs
65 |
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 |
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 | mergePolygons()}>
46 | Merge overlapping features
47 |
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 |
downloadGeojson()}
32 | title="Download data as GeoJSON"
33 | >
34 |
35 |
36 | GeoJSON
37 |
38 |
39 |
josm()}>
40 |
41 |
42 | JOSM
43 |
44 |
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 | modalStatus("none")}
119 | >
120 | Start playing
121 |
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 | setIsModalOpen(true)}
138 | className="custom_button w-full"
139 | >
140 | Create New Project
141 |
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 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 | }
--------------------------------------------------------------------------------