├── .changeset ├── README.md └── config.json ├── .eslintrc ├── .github └── workflows │ ├── intergration.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh.md ├── icon.png ├── media ├── demo1.gif └── demo2.gif ├── package.json ├── pnpm-lock.yaml ├── src ├── extension.ts ├── view-panel.ts └── view │ ├── components │ ├── layer-switch.less │ ├── layer-switch.tsx │ ├── map-container.tsx │ ├── projection-switch.less │ ├── projection-switch.tsx │ ├── props-popup.less │ ├── props-popup.tsx │ └── store.tsx │ ├── index.less │ ├── index.tsx │ ├── tsconfig.json │ ├── utils │ ├── constant.ts │ ├── draw │ │ ├── circle.ts │ │ ├── controls.ts │ │ ├── extend-draw.ts │ │ ├── rectangle.ts │ │ └── simple-select.ts │ ├── measure.ts │ ├── styles.ts │ └── types.ts │ └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ext.js └── vite.config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "privatePackages": { 5 | "version": true, 6 | "tag": true 7 | }, 8 | "commit": false, 9 | "fixed": [], 10 | "linked": [], 11 | "access": "public", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch", 14 | "ignore": [] 15 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "react-hooks", "prettier"], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:react-hooks/recommended", 17 | "prettier" 18 | ], 19 | "rules": { 20 | "@typescript-eslint/no-this-alias": "off", 21 | "@typescript-eslint/naming-convention": "off", 22 | "@typescript-eslint/semi": "off", 23 | "curly": "warn", 24 | "eqeqeq": "warn", 25 | "no-throw-literal": "warn", 26 | "semi": "off", 27 | "prefer-const": "error", 28 | "no-console": "warn", 29 | "react-hooks/rules-of-hooks": "error", 30 | "prettier/prettier": [ 31 | "warn", 32 | { 33 | "endOfLine": "auto", 34 | "singleQuote": true 35 | } 36 | ] 37 | }, 38 | "ignorePatterns": ["out", "dist", "node_modules", "**/*.d.ts"] 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/intergration.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [16.x, 18.x] 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup pnpm 20 | uses: pnpm/action-setup@v2 21 | 22 | - run: pnpm i 23 | - run: pnpm compile 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 18 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 9 24 | 25 | - name: Install Dependencies 26 | run: pnpm install 27 | 28 | - name: Create Release 29 | id: changesets 30 | uses: changesets/action@v1 31 | with: 32 | publish: npx @changesets/cli tag 33 | 34 | - name: Publish to Visual Studio Marketplace 35 | uses: HaaLeo/publish-vscode-extension@v1 36 | with: 37 | pat: ${{secrets.PERSONAL_ACCESS_TOKEN}} 38 | registryUrl: https://marketplace.visualstudio.com 39 | dependencies: false 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts = true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "endOfLine": "auto", 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true 9 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.extension-test-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsdk": "node_modules/typescript/lib", 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll.eslint": "always" 13 | }, 14 | "editor.formatOnSave": true, 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "conventionalCommits.scopes": ["release"] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | .eslintrc 7 | .prettierrc 8 | .changeset 9 | .github 10 | media 11 | vite.config.js 12 | vite.config.ext.js 13 | pnpm-lock.yaml 14 | vsc-extension-quickstart.md 15 | **/tsconfig.json 16 | **/eslint.config.mjs 17 | **/*.map 18 | **/*.ts 19 | **/.vscode-test.* 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - Fix bugs 8 | - Update docs 9 | 10 | ## 1.0.3 11 | 12 | ### Patch Changes 13 | 14 | - Fix bugs caused by thrid party libraries imported. 15 | 16 | ## 1.0.2 17 | 18 | ### Patch Changes 19 | 20 | - Validate GeoJSON Schema before preview it. 21 | 22 | ## 1.0.1 23 | 24 | ### Patch Changes 25 | 26 | - Fix bugs 27 | 28 | ## 1.0.0 29 | 30 | ### Breaking Changes 31 | 32 | - Use mapbox-gl to refactor this extension. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ren Dan 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 |
2 |

GeoJSON.IO for VSCode

3 |

🌍 🗺️ Create, Preview and Edit Your GeoJSON Data in VSCode!

4 |
5 | 6 | install 7 | download 8 | release 9 | update 10 | version 11 | download 12 | 13 |

14 | English | 中文 15 |

16 | 17 | ## Description 18 | 19 | `GeoJSON.IO for VSCode` is an open-source VSCode extension that allows users to easily create, preview, and edit GeoJSON data with VSCode. 20 | 21 | This extension is heavily inspired by [geojson.io](https://geojson.io). 22 | 23 | ## How to use 24 | 25 | 1. Install the extension (obviously 😂). 26 | 27 | 2. Open your GeoJSON file (or a new file) in VSCode. The file extension should be one of these: `.json`, `.txt`, `.geojson`. 28 | 29 | 3. There are two ways to open the map view: 30 | 31 | Press `ctrl + shift + p` (or `cmd + shift + p` on Mac) to open the command palette, type `Preview Geojson`, and hit Enter. 32 | 33 | Alternatively, just right-click in the editor window and select the `🌍 Preview Geojson` option. 34 | 35 | 4. You’re all set to explore! 36 | 37 | ![Demo1](https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/main/media/demo1.gif) 38 | 39 | ![Demo2](https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/main/media/demo2.gif) 40 | 41 | ## Features 42 | 43 | - 🖊️ Real-time editing of collections and properties; 44 | 45 | - 💅 Customizable style rendering 46 | 47 | - 🌍 Supports two projection modes: 3D and Mercator projection 48 | 49 | - 🗺️ Multiple map layers switching 50 | 51 | ## To-Do 52 | 53 | - Custom base map layers 54 | 55 | - More editing modes: snapping, freehand drawing, etc. 56 | 57 | - Label display 58 | 59 | - More... 60 | 61 | ## Other 62 | 63 | If you encounter any issues or have some suggestions, please open an [Issue](https://github.com/42arch/geojson.io-for-vscode/issues/new) on GitHub. 64 | 65 | If the extension has been helpful to you, don’t forget to leave us a [positive review](<(https://marketplace.visualstudio.com/items?itemName=swallow.geojson-io-for-vscode&ssr=false#review-details)>), or leave a 🌟 on [Github](https://github.com/42arch/geojson.io-for-vscode) . Thank you for your support 🙏! 66 | 67 | ## License 68 | 69 | [MIT License](https://github.com/42arch/geojson.io-for-vscode/blob/main/LICENSE) 70 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 |
2 |

GeoJSON.IO for VSCode

3 |

🌍 🗺️ 在 VSCode 中创建,预览和编辑您的 GeoJSON 数据

4 |
5 | 6 | install 7 | download 8 | release 9 | update 10 | version 11 | download 12 | 13 |

14 | English | 中文 15 |

16 | 17 | ## 说明 18 | 19 | `GeoJSON.IO for VSCode` 是一个开源的 VSCode 插件, 可以让使用者在 VSCode 中便捷地创建,预览和编辑 GeoJSON 数据。 20 | 21 | 该插件很大程度上启发自[geojson.io](https://geojson.io),谢谢!。 22 | 23 | ## 如何使用 24 | 25 | 1. 安装这个插件,当然了😂。 26 | 27 | 2. 在VSCode 中打开您的GeoJSON文件(或者空文件),文件的后缀名应该是这些其中之一:`.json`, `.txt`, `.geojson`。 28 | 29 | 3. 有两种方式打开地图窗口: 30 | 31 | 按住 `ctrl + shift + p` (Mac 上为 `cmd + shift + p`) 组合键打开命令面板,输入`Preview Geojson`并回车; 32 | 33 | 或者只要在编辑器窗口右键,选择 `🌍 Preview Geojson` 选项即可; 34 | 35 | 4. 现在可以自由探索了! 36 | 37 | ![Demo1](https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/main/media/demo1.gif) 38 | 39 | ![Demo2](https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/main/media/demo2.gif) 40 | 41 | ## 特性 42 | 43 | - 🖊️ 支持实时编辑集合和属性信息 44 | 45 | - 💅 支持自定义样式渲染 46 | 47 | - 🌍 支持两种投影模式:3D 和 墨卡托投影 48 | 49 | - 🗺️ 支持多底图切换 50 | 51 | ## 待办 52 | 53 | - 自定义底图 54 | 55 | - 支持更多编辑模式:吸附,自由绘制等 56 | 57 | - 支持自定义标签展示 58 | 59 | - 更多... 60 | 61 | ## 其他 62 | 63 | 如果您在使用中遇到问题或者有好的建议,请在GitHub上给我们提 [Issue](https://github.com/42arch/geojson.io-for-vscode/issues/new)。 64 | 65 | 或者这款插件帮助到了您,请不要忘记给我们 [好评](https://marketplace.visualstudio.com/items?itemName=swallow.geojson-io-for-vscode&ssr=false#review-details),或者在 [Github](https://github.com/42arch/geojson.io-for-vscode) 上点个🌟,感谢您的支持🙏! 66 | 67 | ## 许可 68 | 69 | [MIT License](https://github.com/42arch/geojson.io-for-vscode/blob/main/LICENSE) 70 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/bd458b05cd447bbb7590ac256b650e52c5789ba6/icon.png -------------------------------------------------------------------------------- /media/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/bd458b05cd447bbb7590ac256b650e52c5789ba6/media/demo1.gif -------------------------------------------------------------------------------- /media/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/42arch/geojson.io-for-vscode/bd458b05cd447bbb7590ac256b650e52c5789ba6/media/demo2.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geojson-io-for-vscode", 3 | "displayName": "GeoJSON.IO for VSCode", 4 | "publisher": "swallow", 5 | "author": { 6 | "name": "Ren Dan", 7 | "email": "rend42@163.com", 8 | "url": "https://github.com/42arch" 9 | }, 10 | "description": "Create, Preview and Edit Your GeoJSON Data in VSCode", 11 | "private": true, 12 | "version": "1.0.4", 13 | "icon": "icon.png", 14 | "repository": "https://github.com/42arch/geojson.io-for-vscode", 15 | "homepage": "https://github.com/42arch/geojson.io-for-vscode", 16 | "bugs": { 17 | "url": "https://github.com/42arch/geojson.io-for-vscode/issues/new" 18 | }, 19 | "categories": [ 20 | "Other", 21 | "Visualization", 22 | "Data Science" 23 | ], 24 | "activationEvents": [ 25 | "onStartupFinished", 26 | "onDidChangeTextDocument", 27 | "onDidOpenTextDocument" 28 | ], 29 | "engines": { 30 | "vscode": "^1.96.0" 31 | }, 32 | "main": "./out/extension.js", 33 | "contributes": { 34 | "capabilities": { 35 | "hoverProvider": true 36 | }, 37 | "commands": [ 38 | { 39 | "command": "geojson-io.preview-geojson", 40 | "title": "🌏 Preview GeoJSON" 41 | } 42 | ], 43 | "menus": { 44 | "editor/context": [ 45 | { 46 | "when": "editorFocus && resourceExtname== '.json'", 47 | "command": "geojson-io.preview-geojson", 48 | "group": "navigation" 49 | }, 50 | { 51 | "when": "editorFocus && resourceExtname== '.txt'", 52 | "command": "geojson-io.preview-geojson", 53 | "group": "navigation" 54 | }, 55 | { 56 | "when": "editorFocus && resourceExtname== '.geojson'", 57 | "command": "geojson-io.preview-geojson", 58 | "group": "navigation" 59 | } 60 | ] 61 | } 62 | }, 63 | "scripts": { 64 | "package": "pnpm vsce package --no-dependencies", 65 | "publish": "pnpm vsce publish --no-dependencies", 66 | "vscode:prepublish": "pnpm run compile", 67 | "compile:app": "vite build", 68 | "compile:extension": "vite build --config vite.config.ext.js", 69 | "compile": "npm-run-all -p compile:*", 70 | "watch:app": "vite build --watch", 71 | "watch:extension": "vite build --config vite.config.ext.js --watch", 72 | "watch": "npm-run-all -p watch:*", 73 | "pretest": "pnpm run compile && pnpm run lint", 74 | "lint": "eslint src", 75 | "changeset": "changeset", 76 | "version-pkg": "changeset version" 77 | }, 78 | "devDependencies": { 79 | "@changesets/cli": "^2.27.12", 80 | "@types/geojson": "^7946.0.15", 81 | "@types/geojson-validation": "^1.0.3", 82 | "@types/mapbox-gl": "^3.4.1", 83 | "@types/mapbox__mapbox-gl-draw": "^1.4.8", 84 | "@types/mocha": "^10.0.10", 85 | "@types/node": "20.x", 86 | "@types/vscode": "^1.96.0", 87 | "@typescript-eslint/eslint-plugin": "^8.17.0", 88 | "@typescript-eslint/parser": "^8.17.0", 89 | "@vitejs/plugin-react": "^4.3.4", 90 | "@vscode/test-cli": "^0.0.10", 91 | "@vscode/test-electron": "^2.4.1", 92 | "eslint": "^8.57.1", 93 | "eslint-config-prettier": "^10.0.1", 94 | "eslint-plugin-prettier": "^5.2.2", 95 | "eslint-plugin-react": "^7.37.4", 96 | "eslint-plugin-react-hooks": "^5.1.0", 97 | "less": "^4.2.1", 98 | "npm-run-all": "^4.1.5", 99 | "prettier": "^3.4.2", 100 | "typescript": "^5.7.2", 101 | "vite": "^6.0.7" 102 | }, 103 | "dependencies": { 104 | "@mapbox/mapbox-gl-draw": "^1.5.0", 105 | "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1", 106 | "@turf/turf": "^7.2.0", 107 | "@types/react": "18", 108 | "@types/react-dom": "18", 109 | "geojson-validation": "^1.0.2", 110 | "mapbox-gl": "^3.9.3", 111 | "nanoid": "^5.0.9", 112 | "react": "18", 113 | "react-dom": "18" 114 | } 115 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtensionContext, 3 | TextDocument, 4 | commands, 5 | window, 6 | workspace 7 | } from 'vscode' 8 | import gjv from 'geojson-validation' 9 | import ViewPanel from './view-panel' 10 | 11 | function isValidGeojsonText(text: string) { 12 | try { 13 | const parsed = JSON.parse(text) 14 | return gjv.valid(parsed) 15 | } catch { 16 | return false 17 | } 18 | } 19 | 20 | export function activate(context: ExtensionContext) { 21 | const openMapView = commands.registerCommand( 22 | 'geojson-io.preview-geojson', 23 | () => { 24 | try { 25 | const editor = window.activeTextEditor 26 | if (editor) { 27 | const text = editor.document.getText() 28 | 29 | if (!text || isValidGeojsonText(text)) { 30 | ViewPanel.open(context) 31 | ViewPanel.postMessageToWebview(text) 32 | } else { 33 | window.showWarningMessage( 34 | "🌏 The data you're trying to preview is not a valid GeoJSON." 35 | ) 36 | } 37 | } else { 38 | window.showWarningMessage( 39 | '🌏 You should open your geojson file in the editor!' 40 | ) 41 | } 42 | } catch (error) { 43 | window.showErrorMessage('🌏 Failed to preview this file! ' + error) 44 | } 45 | } 46 | ) 47 | 48 | const saveGeojson = workspace.onDidSaveTextDocument( 49 | (document: TextDocument) => { 50 | const text = document.getText() 51 | ViewPanel.postMessageToWebview(text) 52 | } 53 | ) 54 | 55 | context.subscriptions.push(openMapView) 56 | context.subscriptions.push(saveGeojson) 57 | } 58 | 59 | export function deactivate() {} 60 | -------------------------------------------------------------------------------- /src/view-panel.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { 3 | ExtensionContext, 4 | Uri, 5 | ViewColumn, 6 | WebviewPanel, 7 | window, 8 | Disposable, 9 | Range, 10 | workspace 11 | } from 'vscode' 12 | 13 | type Message = { 14 | type: 'error' | 'info' | 'warning' | 'data' 15 | data: string 16 | } 17 | 18 | export default class ViewPanel { 19 | private static instance: ViewPanel | undefined 20 | public panel: WebviewPanel 21 | private context: ExtensionContext 22 | private disposables: Disposable[] 23 | 24 | private constructor(context: ExtensionContext) { 25 | this.context = context 26 | this.disposables = [] 27 | this.panel = window.createWebviewPanel( 28 | 'map-view', 29 | 'Map View', 30 | ViewColumn.Beside, 31 | { 32 | enableScripts: true, 33 | retainContextWhenHidden: true, 34 | localResourceRoots: [ 35 | Uri.file(path.join(this.context.extensionPath, 'out', 'view')) 36 | ] 37 | } 38 | ) 39 | 40 | this.render() 41 | 42 | this.panel.webview.onDidReceiveMessage( 43 | (message: Message) => { 44 | switch (message.type) { 45 | case 'data': 46 | this.replaceTextContent(message.data) 47 | break 48 | case 'error': 49 | window.showErrorMessage(message.data) 50 | break 51 | case 'warning': 52 | window.showWarningMessage(message.data) 53 | break 54 | default: 55 | break 56 | } 57 | }, 58 | null, 59 | this.disposables 60 | ) 61 | 62 | this.panel.onDidDispose( 63 | () => { 64 | this.dispose() 65 | }, 66 | null, 67 | this.disposables 68 | ) 69 | } 70 | 71 | public dispose() { 72 | ViewPanel.instance = undefined 73 | while (this.disposables.length) { 74 | const x = this.disposables.pop() 75 | if (x) { 76 | x.dispose() 77 | } 78 | } 79 | } 80 | 81 | public static getInstance(context: ExtensionContext) { 82 | if (!ViewPanel.instance) { 83 | ViewPanel.instance = new ViewPanel(context) 84 | } 85 | 86 | return ViewPanel.instance 87 | } 88 | 89 | public static open(context: ExtensionContext) { 90 | const instance = ViewPanel.getInstance(context) 91 | 92 | // const column = window.activeTextEditor 93 | // ? window.activeTextEditor.viewColumn 94 | // : undefined 95 | if (instance.panel) { 96 | instance.panel.reveal() 97 | } 98 | } 99 | 100 | static postMessageToWebview(message: string) { 101 | const instance = ViewPanel.instance 102 | if (instance) { 103 | instance.panel.webview.postMessage(message) 104 | } 105 | } 106 | 107 | private render() { 108 | const bundleScriptPath = this.panel.webview.asWebviewUri( 109 | Uri.file( 110 | path.join(this.context.extensionPath, 'out', 'view', 'bundle.js') 111 | ) 112 | ) 113 | const stylePath = this.panel.webview.asWebviewUri( 114 | Uri.file( 115 | path.join( 116 | this.context.extensionPath, 117 | 'out', 118 | 'view', 119 | 'assets', 120 | 'style.css' 121 | ) 122 | ) 123 | ) 124 | 125 | const html = ` 126 | 127 | 128 | 129 | 130 | 131 | Map App 132 | 133 | 157 | 158 | 159 | 160 |
161 | 164 | 165 | 166 | 167 | ` 168 | this.panel.webview.html = html 169 | } 170 | 171 | private formatJSONString(textContent: string) { 172 | const config = workspace.getConfiguration() 173 | const tabSize = Number(config.get('editor.tabSize')) || 2 174 | return JSON.stringify(JSON.parse(textContent), null, tabSize) 175 | } 176 | 177 | private replaceTextContent(textContent: string) { 178 | const textEditor = window.visibleTextEditors[0] 179 | if (!textEditor) { 180 | return 181 | } 182 | const firstLine = textEditor.document.lineAt(0) 183 | const lastLine = textEditor.document.lineAt( 184 | textEditor.document.lineCount - 1 185 | ) 186 | const textRange = new Range( 187 | 0, 188 | firstLine.range.start.character, 189 | textEditor.document.lineCount - 1, 190 | lastLine.range.end.character 191 | ) 192 | 193 | textEditor.edit((editBuilder) => { 194 | editBuilder.replace(textRange, this.formatJSONString(textContent)) 195 | }) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/view/components/layer-switch.less: -------------------------------------------------------------------------------- 1 | .layer-switch { 2 | position: absolute; 3 | left: 0; 4 | bottom: 35px; 5 | z-index: 99; 6 | display: flex; 7 | font-size: 12px; 8 | 9 | button { 10 | padding: 2px 6px; 11 | background: #fff; 12 | font: inherit; 13 | cursor: pointer; 14 | margin: 0; 15 | border: none; 16 | box-sizing: border-box; 17 | 18 | &.active { 19 | background: #34495e; 20 | color: #fff; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/view/components/layer-switch.tsx: -------------------------------------------------------------------------------- 1 | import './layer-switch.less' 2 | import { useStore } from './store' 3 | import { LAYER_STYLES } from '../utils/constant' 4 | 5 | function LayerSwitch() { 6 | const { layerStyle, setLayerStyle } = useStore() 7 | 8 | return ( 9 |
10 | {LAYER_STYLES.map((s) => ( 11 | 17 | ))} 18 |
19 | ) 20 | } 21 | 22 | export default LayerSwitch 23 | -------------------------------------------------------------------------------- /src/view/components/map-container.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | import mapboxgl, { NavigationControl } from 'mapbox-gl' 3 | import MapboxDraw, { DrawCreateEvent } from '@mapbox/mapbox-gl-draw' 4 | import { createRoot } from 'react-dom/client' 5 | import { bbox } from '@turf/turf' 6 | import { FeatureCollection } from 'geojson' 7 | import { nanoid } from 'nanoid' 8 | import StaticMode from '@mapbox/mapbox-gl-draw-static-mode' 9 | import PropsPopup from './props-popup' 10 | import { useStore } from './store' 11 | import { 12 | EditControl, 13 | SaveCancelControl, 14 | TrashControl 15 | } from '../utils/draw/controls' 16 | import ExtendDraw from '../utils/draw/extend-draw' 17 | import DrawRectangle from '../utils/draw/rectangle' 18 | import DrawCircle from '../utils/draw/circle' 19 | import styles from '../utils/styles' 20 | import { 21 | ACCESS_TOKEN, 22 | DEFAULT_FILL_COLOR, 23 | DEFAULT_FILL_OPACITY, 24 | DEFAULT_STROKE_OPACITY, 25 | DEFAULT_STROKE_WIDTH, 26 | LAYER_STYLES, 27 | PROJECTIONS 28 | } from '../utils/constant' 29 | 30 | function MapContainer() { 31 | const containerRef = useRef(null!) 32 | const mapRef = useRef(null) 33 | const drawRef = useRef(null) 34 | const drawControlRef = useRef(null) 35 | const editRef = useRef(null) 36 | const saveCancelRef = useRef(null) 37 | const trashRef = useRef(null) 38 | const { geojson, setGeojson, projection, layerStyle } = useStore() 39 | const latestGeojson = useRef(null) 40 | 41 | useEffect(() => { 42 | latestGeojson.current = geojson 43 | }, [geojson]) 44 | 45 | const handleEditStart = () => { 46 | drawRef.current?.changeMode('simple_select') 47 | toggleMapData('none') 48 | 49 | saveCancelRef.current?.open() 50 | trashRef.current?.open() 51 | editRef.current?.close() 52 | drawControlRef.current?.close() 53 | 54 | if (latestGeojson.current) { 55 | drawRef.current?.add(latestGeojson.current) 56 | } 57 | } 58 | 59 | const handleEditCancel = () => { 60 | drawRef.current?.changeMode('static') 61 | saveCancelRef.current?.close() 62 | trashRef.current?.close() 63 | editRef.current?.open() 64 | drawControlRef.current?.open() 65 | drawRef.current?.deleteAll() 66 | toggleMapData('visible') 67 | } 68 | 69 | const handleEditSave = () => { 70 | drawRef.current?.changeMode('static') 71 | saveCancelRef.current?.close() 72 | trashRef.current?.close() 73 | editRef.current?.open() 74 | drawControlRef.current?.open() 75 | const features = drawRef.current?.getAll() 76 | if (features) { 77 | updateLayerData(features) 78 | setGeojson(features) 79 | postData(features) 80 | } 81 | 82 | drawRef.current?.deleteAll() 83 | toggleMapData('visible') 84 | } 85 | 86 | const addMapData = (geojson: FeatureCollection) => { 87 | const color = DEFAULT_FILL_COLOR 88 | 89 | mapRef.current?.addSource('map-data', { 90 | type: 'geojson', 91 | data: geojson, 92 | promoteId: '_id' 93 | }) 94 | 95 | mapRef.current?.addLayer({ 96 | id: 'map-data-fill', 97 | type: 'fill', 98 | source: 'map-data', 99 | paint: { 100 | 'fill-color': ['coalesce', ['get', 'fill'], color], 101 | 'fill-opacity': [ 102 | 'coalesce', 103 | ['get', 'fill-opacity'], 104 | DEFAULT_FILL_OPACITY 105 | ] 106 | }, 107 | filter: ['==', ['geometry-type'], 'Polygon'] 108 | }) 109 | 110 | mapRef.current?.addLayer({ 111 | id: 'map-data-fill-outline', 112 | type: 'line', 113 | source: 'map-data', 114 | paint: { 115 | 'line-color': ['coalesce', ['get', 'stroke'], color], 116 | 'line-width': [ 117 | 'coalesce', 118 | ['get', 'stroke-width'], 119 | DEFAULT_STROKE_WIDTH 120 | ], 121 | 'line-opacity': [ 122 | 'coalesce', 123 | ['get', 'stroke-opacity'], 124 | DEFAULT_STROKE_OPACITY 125 | ] 126 | }, 127 | filter: ['==', ['geometry-type'], 'Polygon'] 128 | }) 129 | 130 | mapRef.current?.addLayer({ 131 | id: 'map-data-line', 132 | type: 'line', 133 | source: 'map-data', 134 | paint: { 135 | 'line-color': ['coalesce', ['get', 'stroke'], color], 136 | 'line-width': [ 137 | 'coalesce', 138 | ['get', 'stroke-width'], 139 | DEFAULT_STROKE_WIDTH 140 | ], 141 | 'line-opacity': [ 142 | 'coalesce', 143 | ['get', 'stroke-opacity'], 144 | DEFAULT_STROKE_OPACITY 145 | ] 146 | }, 147 | filter: ['==', ['geometry-type'], 'LineString'] 148 | }) 149 | 150 | mapRef.current?.addLayer({ 151 | id: 'map-data-point', 152 | type: 'circle', 153 | source: 'map-data', 154 | paint: { 155 | 'circle-color': ['coalesce', ['get', 'fill'], color], 156 | 'circle-opacity': [ 157 | 'coalesce', 158 | ['get', 'fill-opacity'], 159 | DEFAULT_FILL_OPACITY 160 | ], 161 | 'circle-stroke-width': [ 162 | 'coalesce', 163 | ['get', 'stroke-width'], 164 | DEFAULT_STROKE_WIDTH 165 | ], 166 | 'circle-stroke-color': ['coalesce', ['get', 'stroke'], '#ffffff'] 167 | }, 168 | filter: ['==', ['geometry-type'], 'Point'] 169 | }) 170 | } 171 | 172 | const toggleMapData = (visibility: 'visible' | 'none') => { 173 | const map = mapRef.current 174 | map?.setLayoutProperty('map-data-fill', 'visibility', visibility) 175 | map?.setLayoutProperty('map-data-fill-outline', 'visibility', visibility) 176 | map?.setLayoutProperty('map-data-line', 'visibility', visibility) 177 | map?.setLayoutProperty('map-data-point', 'visibility', visibility) 178 | } 179 | 180 | const created = useCallback( 181 | (e: DrawCreateEvent) => { 182 | if (!geojson || !mapRef.current) { 183 | return 184 | } 185 | const fc = geojson 186 | e.features.forEach((feature) => { 187 | feature.properties = { 188 | ...feature.properties, 189 | _id: nanoid() 190 | } 191 | }) 192 | fc.features = [...fc.features, ...e.features] 193 | 194 | if (!fc) { 195 | return 196 | } 197 | 198 | updateLayerData(fc) 199 | setGeojson(fc) 200 | postData(fc) 201 | drawRef.current?.deleteAll() 202 | drawRef.current?.changeMode('static') 203 | }, 204 | [geojson, setGeojson] 205 | ) 206 | 207 | useEffect(() => { 208 | mapboxgl.accessToken = ACCESS_TOKEN 209 | if (mapRef.current) { 210 | return 211 | } 212 | mapRef.current = new mapboxgl.Map({ 213 | container: containerRef.current, 214 | center: [0, 0], 215 | zoom: 1, 216 | style: LAYER_STYLES[0].style, 217 | projection: PROJECTIONS[0].value 218 | }) 219 | 220 | drawRef.current = new MapboxDraw({ 221 | displayControlsDefault: false, 222 | modes: { 223 | ...MapboxDraw.modes, 224 | // simple_select: SimpleSelect, 225 | // direct_select: MapboxDraw.modes.direct_select, 226 | draw_rectangle: DrawRectangle, 227 | draw_circle: DrawCircle, 228 | static: StaticMode 229 | }, 230 | controls: {}, 231 | defaultMode: 'static', 232 | userProperties: true, 233 | styles: styles 234 | }) 235 | 236 | mapRef.current.addControl(new NavigationControl(), 'top-right') 237 | 238 | const draw = drawRef.current 239 | const drawControl = new ExtendDraw({ 240 | draw: draw, 241 | buttons: [ 242 | { 243 | on: 'click', 244 | action: () => { 245 | // drawing = true 246 | draw.changeMode('draw_point') 247 | }, 248 | classes: ['mapbox-gl-draw_ctrl-draw-btn', 'mapbox-gl-draw_point'], 249 | title: 'Draw Point' 250 | }, 251 | { 252 | on: 'click', 253 | action: () => { 254 | draw.changeMode('draw_line_string') 255 | }, 256 | classes: ['mapbox-gl-draw_ctrl-draw-btn', 'mapbox-gl-draw_line'], 257 | title: 'Draw LineString' 258 | }, 259 | { 260 | on: 'click', 261 | action: () => { 262 | draw.changeMode('draw_polygon') 263 | }, 264 | classes: ['mapbox-gl-draw_ctrl-draw-btn', 'mapbox-gl-draw_polygon'], 265 | title: 'Draw Polygon' 266 | }, 267 | { 268 | on: 'click', 269 | action: () => { 270 | draw.changeMode('draw_rectangle') 271 | }, 272 | classes: ['mapbox-gl-draw_ctrl-draw-btn', 'mapbox-gl-draw_rectangle'], 273 | title: 'Draw Rectangular Polygon' 274 | }, 275 | { 276 | on: 'click', 277 | action: () => { 278 | draw.changeMode('draw_circle') 279 | }, 280 | classes: ['mapbox-gl-draw_ctrl-draw-btn', 'mapbox-gl-draw_circle'], 281 | title: 'Draw Circular Polygon' 282 | } 283 | ] 284 | }) 285 | drawControlRef.current = drawControl 286 | mapRef.current.addControl(drawControl) 287 | 288 | editRef.current = new EditControl() 289 | mapRef.current.addControl(editRef.current, 'top-right') 290 | 291 | saveCancelRef.current = new SaveCancelControl() 292 | mapRef.current.addControl(saveCancelRef.current, 'top-right') 293 | 294 | trashRef.current = new TrashControl() 295 | mapRef.current.addControl(trashRef.current, 'top-right') 296 | 297 | editRef.current?.onClick(handleEditStart) 298 | 299 | saveCancelRef.current.onCancelClick(handleEditCancel) 300 | 301 | saveCancelRef.current.onSaveClick(handleEditSave) 302 | 303 | trashRef.current.onClick(() => { 304 | drawRef.current?.trash() 305 | 306 | console.log('trash', geojson, latestGeojson.current) 307 | }) 308 | 309 | mapRef.current.on('click', (e) => { 310 | const features = mapRef.current?.queryRenderedFeatures(e.point, { 311 | layers: ['map-data-point', 'map-data-line', 'map-data-fill'] 312 | }) 313 | 314 | if (features && features.length > 0) { 315 | const popupNode = document.createElement('div') 316 | 317 | const popup = new mapboxgl.Popup() 318 | .setLngLat(e.lngLat) 319 | .setDOMContent(popupNode) 320 | .addTo(mapRef.current!) 321 | 322 | createRoot(popupNode).render( 323 | { 326 | latestGeojson.current?.features.forEach((feature) => { 327 | if (feature.properties && feature.properties['_id'] === id) { 328 | feature.properties = properties 329 | } 330 | }) 331 | if (latestGeojson.current) { 332 | setGeojson(latestGeojson.current) 333 | } 334 | updateLayerData(latestGeojson.current) 335 | postData(latestGeojson.current) 336 | popup.remove() 337 | }} 338 | onCancel={() => popup.remove()} 339 | /> 340 | ) 341 | } 342 | }) 343 | 344 | return () => { 345 | mapRef.current?.remove() 346 | } 347 | }, []) 348 | 349 | useEffect(() => { 350 | mapRef.current?.on('idle', () => { 351 | if (!mapRef.current?.getSource('map-data')) { 352 | if (latestGeojson.current) { 353 | const [minLng, minLat, maxLng, maxLat] = bbox(latestGeojson.current) 354 | mapRef.current?.fitBounds([minLng, minLat, maxLng, maxLat], { 355 | padding: 10 356 | }) 357 | addMapData(latestGeojson.current) 358 | } 359 | } 360 | }) 361 | 362 | if (mapRef.current?.isStyleLoaded()) { 363 | updateLayerData(latestGeojson.current) 364 | } 365 | 366 | mapRef.current?.on('draw.create', created) 367 | 368 | return () => { 369 | mapRef.current?.off('draw.create', created) 370 | } 371 | }, [geojson]) 372 | 373 | useEffect(() => { 374 | mapRef.current?.setProjection(projection) 375 | }, [projection]) 376 | 377 | useEffect(() => { 378 | mapRef.current?.setStyle(layerStyle.style) 379 | }, [layerStyle]) 380 | 381 | const updateLayerData = (geojson: FeatureCollection | null) => { 382 | if (!geojson || !mapRef.current) { 383 | return 384 | } 385 | 386 | const source = mapRef.current.getSource( 387 | 'map-data' 388 | ) as mapboxgl.GeoJSONSource 389 | 390 | if (source) { 391 | source.setData(geojson) 392 | } 393 | } 394 | 395 | return ( 396 |
401 | ) 402 | } 403 | 404 | export default MapContainer 405 | 406 | function postData(fc: FeatureCollection | null) { 407 | if (!fc) { 408 | return 409 | } 410 | const newFc = { 411 | ...fc, 412 | features: fc.features.map((f) => { 413 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 414 | const { _id, ...rest } = { ...f.properties } 415 | return { 416 | ...f, 417 | properties: rest 418 | } 419 | }) 420 | } 421 | 422 | vscode.postMessage({ 423 | type: 'data', 424 | data: JSON.stringify(newFc) 425 | }) 426 | } 427 | -------------------------------------------------------------------------------- /src/view/components/projection-switch.less: -------------------------------------------------------------------------------- 1 | .projection-switch { 2 | position: absolute; 3 | left: 0; 4 | bottom: 60px; 5 | z-index: 99; 6 | display: flex; 7 | font-size: 12px; 8 | 9 | button { 10 | padding: 2px 6px; 11 | background: #fff; 12 | font: inherit; 13 | cursor: pointer; 14 | margin: 0; 15 | border: none; 16 | box-sizing: border-box; 17 | 18 | &.active { 19 | background: #34495e; 20 | color: #fff; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/view/components/projection-switch.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from './store' 2 | import { PROJECTIONS } from '../utils/constant' 3 | import './projection-switch.less' 4 | 5 | function ProjectionSwitch() { 6 | const { projection, setProjection } = useStore() 7 | 8 | return ( 9 |
10 | {PROJECTIONS.map((p) => ( 11 | 17 | ))} 18 |
19 | ) 20 | } 21 | 22 | export default ProjectionSwitch 23 | -------------------------------------------------------------------------------- /src/view/components/props-popup.less: -------------------------------------------------------------------------------- 1 | .props-popup { 2 | min-width: 180px; 3 | min-height: 60px; 4 | z-index: 99; 5 | 6 | table { 7 | font-size: 12px; 8 | font-weight: 400; 9 | border-spacing: 0; 10 | border-collapse: collapse; 11 | width: 100%; 12 | overflow: auto; 13 | border-bottom: 1px solid #ccc; 14 | 15 | tbody { 16 | display: block; 17 | max-height: 180px; 18 | overflow-y: auto; 19 | 20 | tr { 21 | &:last-child { 22 | td { 23 | border-bottom: none; 24 | } 25 | } 26 | 27 | &:nth-child(even) { 28 | td { 29 | background-color: #f7f7f7; 30 | } 31 | } 32 | 33 | td { 34 | border: 1px solid #ccc; 35 | padding: 0 3px; 36 | 37 | input { 38 | width: 100px; 39 | border: 0; 40 | outline: 0; 41 | background: transparent; 42 | height: 24px; 43 | line-height: 24px; 44 | font-size: 11px; 45 | 46 | &:focus-visible { 47 | border: 0; 48 | outline: 0; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | .properties-table { 57 | .props-opts { 58 | width: 100%; 59 | height: 32px; 60 | display: flex; 61 | justify-content: space-between; 62 | align-items: center; 63 | 64 | #add-row { 65 | width: 35%; 66 | display: flex; 67 | align-items: center; 68 | } 69 | 70 | #add-row span { 71 | cursor: pointer; 72 | } 73 | 74 | #add-style-props { 75 | cursor: pointer; 76 | &:hover { 77 | } 78 | } 79 | } 80 | } 81 | 82 | .content { 83 | padding: 10px 10px 0 10px; 84 | } 85 | 86 | .footer { 87 | .tabs { 88 | width: 100%; 89 | display: flex; 90 | justify-content: space-between; 91 | 92 | .tab_item { 93 | margin-top: 10px; 94 | width: 50%; 95 | height: 28px; 96 | line-height: 28px; 97 | text-align: center; 98 | border: 1px solid #ccc; 99 | cursor: pointer; 100 | background: #eee; 101 | 102 | &:first-child { 103 | border-left: none; 104 | } 105 | 106 | &:last-child { 107 | border-right: none; 108 | } 109 | 110 | &.active { 111 | background: transparent; 112 | border-left: 1px solid transparent; 113 | border-right: 1px solid transparent; 114 | border-top: 1px solid transparent; 115 | } 116 | } 117 | } 118 | 119 | .opts { 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | margin-top: 6px; 124 | margin-bottom: 6px; 125 | height: 32px; 126 | 127 | .btns { 128 | display: flex; 129 | 130 | button { 131 | cursor: pointer; 132 | border: 0; 133 | border-radius: 1px; 134 | background-color: #59aae7; 135 | color: #ffffff; 136 | font-size: 12px; 137 | } 138 | 139 | .save-btn { 140 | padding: 6px; 141 | border-radius: 3px; 142 | color: white; 143 | background: #34495e; 144 | border-radius: 3px 0 0 3px; 145 | 146 | &:hover { 147 | background: #2980b9; 148 | } 149 | } 150 | 151 | .cancel-btn { 152 | padding: 6px; 153 | background: #eee; 154 | color: #222222; 155 | border-radius: 0 3px 3px 0; 156 | 157 | &:hover { 158 | background: #f7f7f7; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/view/components/props-popup.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | forwardRef, 3 | useCallback, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | useState 8 | } from 'react' 9 | import { GeoJSONFeature } from 'mapbox-gl' 10 | import { GeoJsonGeometryTypes } from 'geojson' 11 | import { 12 | DEFAULT_FILL_STYLE_ROWS, 13 | DEFAULT_LINE_STYLE_ROWS, 14 | DEFAULT_POINT_STYLE_ROWS, 15 | STYLE_FIELDS 16 | } from '../utils/constant' 17 | import measure from '../utils/measure' 18 | import { Row } from '../utils/types' 19 | import './props-popup.less' 20 | 21 | interface Props { 22 | data: GeoJSONFeature 23 | onSave: ( 24 | id: string | number | undefined, 25 | properties: GeoJSONFeature['properties'] 26 | ) => void 27 | onCancel: () => void 28 | } 29 | 30 | // Value Input 31 | const ValueInput = forwardRef< 32 | HTMLInputElement, 33 | { 34 | field: string 35 | value: string | number 36 | onChange: (value: string | number) => void 37 | } 38 | >(({ field, value, onChange }, ref) => { 39 | switch (field) { 40 | case 'stroke': 41 | case 'fill': 42 | return ( 43 | { 48 | onChange(e.target.value) 49 | }} 50 | /> 51 | ) 52 | case 'stroke-width': 53 | return ( 54 | { 60 | onChange(e.target.valueAsNumber) 61 | }} 62 | /> 63 | ) 64 | case 'stroke-opacity': 65 | case 'fill-opacity': 66 | return ( 67 | { 75 | onChange(e.target.valueAsNumber) 76 | }} 77 | /> 78 | ) 79 | default: 80 | return ( 81 | { 86 | onChange(e.target.value) 87 | }} 88 | /> 89 | ) 90 | } 91 | }) 92 | 93 | // Properties Table 94 | function PropertiesTable({ 95 | type, 96 | rowList, 97 | onChange 98 | }: { 99 | type: GeoJsonGeometryTypes 100 | rowList: Row[] 101 | onChange: (rowList: Row[]) => void 102 | }) { 103 | useEffect(() => { 104 | onChange(rowList) 105 | }, [rowList]) 106 | 107 | const rowRefs = useRef< 108 | { key: HTMLInputElement | null; value: HTMLInputElement | null }[] 109 | >([]) 110 | 111 | const addNewRow = () => { 112 | const newRowList = [...rowList, { field: '', value: '' }] 113 | onChange(newRowList) 114 | } 115 | 116 | const addStyleProperties = () => { 117 | const newRowList = addStyleRows(type, rowList) 118 | onChange(newRowList) 119 | } 120 | 121 | const handleChange = ( 122 | index: number, 123 | field: 'field' | 'value', 124 | newValue: string | number 125 | ) => { 126 | const newRowList = [...rowList] 127 | newRowList[index] = { 128 | ...newRowList[index], 129 | [field]: newValue 130 | } 131 | onChange(newRowList) 132 | } 133 | 134 | return ( 135 |
136 | 137 | 138 | {rowList 139 | .filter((row) => row.field !== '_id') 140 | .map((row, index) => ( 141 | 142 | 157 | 172 | 173 | ))} 174 | 175 |
143 | 147 | (rowRefs.current[index] = { 148 | ...rowRefs.current[index], 149 | key: el 150 | }) 151 | } 152 | onChange={(e) => { 153 | handleChange(index, 'field', e.target.value) 154 | }} 155 | /> 156 | 158 | 162 | (rowRefs.current[index] = { 163 | ...rowRefs.current[index], 164 | value: el 165 | }) 166 | } 167 | onChange={(v) => { 168 | handleChange(index, 'value', v) 169 | }} 170 | /> 171 |
176 |
177 |
178 | 179 | + 180 | Add row 181 | 182 |
183 | {!STYLE_FIELDS.every((field) => 184 | rowList.map((r) => r.field).includes(field) 185 | ) && ( 186 | 187 | Add style properties 188 | 189 | )} 190 |
191 |
192 | ) 193 | } 194 | 195 | // Measure Info Table 196 | function InfoTable({ data }: { data: GeoJSONFeature }) { 197 | const info = measure(data) 198 | 199 | return ( 200 |
201 | 202 | 203 | {Object.keys(info).map((field, index) => ( 204 | 205 | 208 | 211 | 212 | ))} 213 | 214 |
206 | 207 | 209 | 210 |
215 |
216 | ) 217 | } 218 | 219 | function PropsPopup({ data, onSave, onCancel }: Props) { 220 | const [activeTab, setActiveTab] = useState<'properties' | 'info'>( 221 | 'properties' 222 | ) 223 | 224 | const id = useMemo(() => { 225 | return data.properties ? data.properties['_id'] : '' 226 | }, [data]) 227 | 228 | const [rowList, setRowList] = useState([]) 229 | 230 | useEffect(() => { 231 | if (data.properties) { 232 | const keys = Object.keys(data.properties).filter((key) => key !== '_id') 233 | if (keys.length) { 234 | setRowList( 235 | keys.map((key) => ({ 236 | field: key, 237 | value: data.properties ? data.properties[key] : '' 238 | })) 239 | ) 240 | } else { 241 | setRowList([{ field: '', value: '' }]) 242 | } 243 | } else { 244 | setRowList([{ field: '', value: '' }]) 245 | } 246 | }, [data]) 247 | 248 | const handleSave = useCallback(() => { 249 | const properties = rowList.reduce((acc, cur) => { 250 | return cur.field ? { ...acc, [cur.field]: cur.value } : acc 251 | }, {}) 252 | 253 | onSave(id, { 254 | ...properties, 255 | _id: id 256 | }) 257 | }, [id, onSave, rowList]) 258 | 259 | return ( 260 |
261 |
262 | {activeTab === 'properties' ? ( 263 | { 267 | setRowList(rowList) 268 | }} 269 | /> 270 | ) : ( 271 | 272 | )} 273 |
274 |
275 |
276 |
setActiveTab('properties')}> 279 | Properties 280 |
281 |
setActiveTab('info')}> 284 | Info 285 |
286 |
287 |
288 |
289 | 292 | 295 |
296 |
297 |
298 |
299 | ) 300 | } 301 | 302 | export default PropsPopup 303 | 304 | function addStyleRows(type: GeoJsonGeometryTypes, rowList: Row[]) { 305 | const mergeRowList = (rowList: Row[], styleRowList: Row[]) => { 306 | const merged = [...rowList.filter((r) => r.field)] 307 | styleRowList.forEach((row) => { 308 | const exist = merged.find((r) => r.field === row.field) 309 | if (!exist) { 310 | merged.push(row) 311 | } 312 | }) 313 | return merged 314 | } 315 | 316 | switch (type) { 317 | case 'Point': 318 | case 'MultiPoint': 319 | return mergeRowList(rowList, DEFAULT_POINT_STYLE_ROWS) 320 | case 'LineString': 321 | case 'MultiLineString': 322 | return mergeRowList(rowList, DEFAULT_LINE_STYLE_ROWS) 323 | case 'Polygon': 324 | case 'MultiPolygon': 325 | return mergeRowList(rowList, DEFAULT_FILL_STYLE_ROWS) 326 | default: 327 | return [...rowList] 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/view/components/store.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, PropsWithChildren, useContext, useState } from 'react' 2 | import { FeatureCollection } from 'geojson' 3 | import { MapOptions, ProjectionSpecification } from 'mapbox-gl' 4 | import { LayerStyle } from '../utils/types' 5 | import { LAYER_STYLES } from '../utils/constant' 6 | 7 | interface StoreProps { 8 | geojson: FeatureCollection | null 9 | setGeojson: (geojson: FeatureCollection) => void 10 | projection: ProjectionSpecification['name'] 11 | setProjection: (projection: ProjectionSpecification['name']) => void 12 | layerStyle: LayerStyle 13 | setLayerStyle: (style: LayerStyle) => void 14 | } 15 | 16 | const Store = createContext({} as StoreProps) 17 | 18 | export function StoreProvider({ children }: PropsWithChildren) { 19 | const [geojson, setGeojson] = useState(null) 20 | const [projection, setProjection] = 21 | useState('globe') 22 | const [layerStyle, setLayerStyle] = useState(LAYER_STYLES[0]) 23 | 24 | return ( 25 | 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | export function useStore() { 40 | return useContext(Store) 41 | } 42 | -------------------------------------------------------------------------------- /src/view/index.less: -------------------------------------------------------------------------------- 1 | // #root { 2 | // width: 100%; 3 | // height: 100%; 4 | // } 5 | 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | scrollbar-color: rgba(0, 0, 0, 0.2) !important; 13 | 14 | &::-webkit-scrollbar { 15 | width: 5px !important; 16 | height: 10px !important; 17 | } 18 | 19 | &::-webkit-scrollbar-thumb { 20 | background: rgba(0, 0, 0, 0.2) !important; 21 | } 22 | 23 | &::-webkit-scrollbar-track { 24 | background: transparent !important; 25 | } 26 | } 27 | 28 | #root { 29 | width: 100%; 30 | height: 100%; 31 | font-size: 12px; 32 | } 33 | 34 | .app { 35 | color: royalblue; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | // override 41 | 42 | .mapboxgl-popup-content { 43 | padding: 0; 44 | } 45 | 46 | .mapboxgl-popup-close-button { 47 | display: none; 48 | } 49 | 50 | // controls 51 | .mapbox-gl-draw_polygon { 52 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJzaGFwZS5zdmciPiAgPGRlZnMgICAgIGlkPSJkZWZzMTkxNjkiIC8+ICA8c29kaXBvZGk6bmFtZWR2aWV3ICAgICBpZD0iYmFzZSIgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIgICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiICAgICBpbmtzY2FwZTp6b29tPSIzMiIgICAgIGlua3NjYXBlOmN4PSIxMy4zMjIyNjMiICAgICBpbmtzY2FwZTpjeT0iMTAuOTE0MzY2IiAgICAgaW5rc2NhcGU6ZG9jdW1lbnQtdW5pdHM9InB4IiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIiAgICAgc2hvd2dyaWQ9InRydWUiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTEyIiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjIzIiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIgICAgIGlua3NjYXBlOm9iamVjdC1ub2Rlcz0idHJ1ZSI+ICAgIDxpbmtzY2FwZTpncmlkICAgICAgIHR5cGU9Inh5Z3JpZCIgICAgICAgaWQ9ImdyaWQxOTcxNSIgLz4gIDwvc29kaXBvZGk6bmFtZWR2aWV3PiAgPG1ldGFkYXRhICAgICBpZD0ibWV0YWRhdGExOTE3MiI+ICAgIDxyZGY6UkRGPiAgICAgIDxjYzpXb3JrICAgICAgICAgcmRmOmFib3V0PSIiPiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+ICAgICAgICA8ZGM6dHlwZSAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4gICAgICAgIDxkYzp0aXRsZSAvPiAgICAgIDwvY2M6V29yaz4gICAgPC9yZGY6UkRGPiAgPC9tZXRhZGF0YT4gIDxnICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIgICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiICAgICBpZD0ibGF5ZXIxIiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTAzMi4zNjIyKSI+ICAgIDxwYXRoICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MC41O21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Im0gNS4wNTU0NTYsMTAxOS4wMTA0IDAsNiAyLDIgNiwwIDIsLTIgMCwtNiAtMiwtMiAtNiwwIHogbSAzLDAgNCwwIDEsMSAwLDQgLTEsMSAtNCwwIC0xLC0xIDAsLTQgeiIgICAgICAgaWQ9InJlY3Q3Nzk3IiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjY2NjY2NjY2NjY2NjY2NjYyIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9InBhdGg0MzY0IiAgICAgICBjeD0iNi4wNTU0NTYyIiAgICAgICBjeT0iMTAyNi4wMTA0IiAgICAgICByPSIyIiAvPiAgICA8Y2lyY2xlICAgICAgIGlkPSJwYXRoNDM2OCIgICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjYwMDAwMDAyO21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGN4PSIxNC4wNTU0NTYiICAgICAgIGN5PSIxMDI2LjAxMDQiICAgICAgIHI9IjIiIC8+ICAgIDxjaXJjbGUgICAgICAgaWQ9InBhdGg0MzcwIiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgY3g9IjYuMDU1NDU2MiIgICAgICAgY3k9IjEwMTguMDEwNCIgICAgICAgcj0iMiIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9InBhdGg0MzcyIiAgICAgICBjeD0iMTQuMDU1NDU2IiAgICAgICBjeT0iMTAxOC4wMTA0IiAgICAgICByPSIyIiAvPiAgICA8Y2lyY2xlICAgICAgIHI9IjIiICAgICAgIGN5PSIxMDE2LjM2MjIiICAgICAgIGN4PSIzMCIgICAgICAgaWQ9ImNpcmNsZTE2NjQ1IiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgLz4gICAgPHBhdGggICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDowLjU7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAyNC4wNTU0NTYsMTAyMC4wMTA0IDIsNSAyLDIgNCwwIDIsLTIgMiwtNSAtMiwtMiAtOCwwIHogbSA0LDAgNCwwIDEsMSAtMSwzIC0xLDEgLTIsMCAtMSwtMSAtMSwtMyB6IiAgICAgICBpZD0icGF0aDE2NjQ3IiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjY2NjY2NjY2NjY2NjY2NjYyIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9ImNpcmNsZTE2NjQ5IiAgICAgICBjeD0iMjcuMDU1NDU4IiAgICAgICBjeT0iMTAyNi4wMTA0IiAgICAgICByPSIyIiAvPiAgICA8Y2lyY2xlICAgICAgIGlkPSJjaXJjbGUxNjY1MSIgICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjYwMDAwMDAyO21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGN4PSIzMy4wNTU0NTgiICAgICAgIGN5PSIxMDI2LjAxMDQiICAgICAgIHI9IjIiIC8+ICAgIDxjaXJjbGUgICAgICAgaWQ9ImNpcmNsZTE2NjUzIiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgY3g9IjI1LjA1NTQ1OCIgICAgICAgY3k9IjEwMTkuMDEwNCIgICAgICAgcj0iMiIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9ImNpcmNsZTE2NjU1IiAgICAgICBjeD0iMzUuMDU1NDU4IiAgICAgICBjeT0iMTAxOS4wMTA0IiAgICAgICByPSIyIiAvPiAgICA8cGF0aCAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBpZD0icGF0aDE2NjU3IiAgICAgICBkPSJtIDMwLDEwMTUuMzYyMiA1LDQgLTEwLDAgeiIgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7Zm9udC1zaXplOm1lZGl1bTtsaW5lLWhlaWdodDpub3JtYWw7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjt0ZXh0LWluZGVudDowO3RleHQtYWxpZ246c3RhcnQ7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC1kZWNvcmF0aW9uLWxpbmU6bm9uZTt0ZXh0LWRlY29yYXRpb24tc3R5bGU6c29saWQ7dGV4dC1kZWNvcmF0aW9uLWNvbG9yOiMwMDAwMDA7bGV0dGVyLXNwYWNpbmc6bm9ybWFsO3dvcmQtc3BhY2luZzpub3JtYWw7dGV4dC10cmFuc2Zvcm06bm9uZTtkaXJlY3Rpb246bHRyO2Jsb2NrLXByb2dyZXNzaW9uOnRiO3dyaXRpbmctbW9kZTpsci10YjtiYXNlbGluZS1zaGlmdDpiYXNlbGluZTt0ZXh0LWFuY2hvcjpzdGFydDt3aGl0ZS1zcGFjZTpub3JtYWw7c2hhcGUtcGFkZGluZzowO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOmJldmVsO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Im0gNDkuNDg2MzI4LDEwMTUuNTAzOSAtNSwzIC0wLjQzNTU0NywxLjE3MzggMiw2IDAuOTQ5MjE5LDAuNjgzNiA2LDAgMC45NDkyMTksLTAuNjgzNiAyLC02IC0wLjQzNTU0NywtMS4xNzM4IC01LC0zIC0xLjAyNzM0NCwwIHogTSA1MCwxMDE3LjUyOTMgbCAzLjc5Njg3NSwyLjI3NzMgLTEuNTE3NTc4LDQuNTU0NyAtNC41NTg1OTQsMCAtMS41MTc1NzgsLTQuNTU0NyBMIDUwLDEwMTcuNTI5MyBaIiAgICAgICBpZD0icGF0aDE3Mzc1IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPiAgICA8Y2lyY2xlICAgICAgIHI9IjIiICAgICAgIGN5PSIxMDE2LjM2MjIiICAgICAgIGN4PSI1MCIgICAgICAgaWQ9ImNpcmNsZTE3Mzc3IiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9ImNpcmNsZTE3Mzc5IiAgICAgICBjeD0iNDcuMDU1NDU4IiAgICAgICBjeT0iMTAyNi4wMTA0IiAgICAgICByPSIyIiAvPiAgICA8Y2lyY2xlICAgICAgIGlkPSJjaXJjbGUxNzM4MSIgICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjYwMDAwMDAyO21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGN4PSI1My4wNTU0NTgiICAgICAgIGN5PSIxMDI2LjAxMDQiICAgICAgIHI9IjIiIC8+ICAgIDxjaXJjbGUgICAgICAgaWQ9ImNpcmNsZTE3MzgzIiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgY3g9IjQ1LjA1NTQ1OCIgICAgICAgY3k9IjEwMTkuMzYyMiIgICAgICAgcj0iMiIgLz4gICAgPGNpcmNsZSAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgaWQ9ImNpcmNsZTE3Mzg1IiAgICAgICBjeD0iNTUuMDU1NDU4IiAgICAgICBjeT0iMTAxOS4zNjIyIiAgICAgICByPSIyIiAvPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2ZvbnQtc2l6ZTptZWRpdW07bGluZS1oZWlnaHQ6bm9ybWFsO2ZvbnQtZmFtaWx5OnNhbnMtc2VyaWY7dGV4dC1pbmRlbnQ6MDt0ZXh0LWFsaWduOnN0YXJ0O3RleHQtZGVjb3JhdGlvbjpub25lO3RleHQtZGVjb3JhdGlvbi1saW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uLXN0eWxlOnNvbGlkO3RleHQtZGVjb3JhdGlvbi1jb2xvcjojMDAwMDAwO2xldHRlci1zcGFjaW5nOm5vcm1hbDt3b3JkLXNwYWNpbmc6bm9ybWFsO3RleHQtdHJhbnNmb3JtOm5vbmU7ZGlyZWN0aW9uOmx0cjtibG9jay1wcm9ncmVzc2lvbjp0Yjt3cml0aW5nLW1vZGU6bHItdGI7YmFzZWxpbmUtc2hpZnQ6YmFzZWxpbmU7dGV4dC1hbmNob3I6c3RhcnQ7d2hpdGUtc3BhY2U6bm9ybWFsO3NoYXBlLXBhZGRpbmc6MDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjpiZXZlbDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDkuNTAzNTc1MSwxMDM2Ljc2NjMgLTQuOTk5OTEyNiwzIC0wLjQzNTM3MTMsMS4xNzM5IDEuOTk5OTczMSw1Ljk5OTkgMC45NDkyMjk4LDAuNjgzNiA1Ljk5OTg5ODksMCAwLjk0OTI3NSwtMC42ODM1IDIuMDAwMDE2LC02IC0wLjQzNTU0NywtMS4xNzM4IC01LjAwMDAyMywtMy4wMDAxIC0xLjAyNzMyMzgsMTBlLTUgeiBtIC0wLjQ4NjI5MTEsMi44NTc0IDEuOTk5OTQ4LC0xZS00IDEuOTk5OTg3LDIgOC44ZS01LDIuMDAwMSAtMi4wMDAwMDIsMiAtMS45OTk5ODM1LDAgLTEuOTk5OTg3NCwtMiAtMS4yNThlLTQsLTIuMDAwMSAyLjAwMDA3NTcsLTEuOTk5OSB6IiAgICAgICBpZD0icGF0aDE3Mzk4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPiAgICA8Y2lyY2xlICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MS42MDAwMDAwMjttYXJrZXI6bm9uZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBpZD0iY2lyY2xlMTc0MDAiICAgICAgIGN4PSIxMCIgICAgICAgY3k9IjEwMzcuMzYyMiIgICAgICAgcj0iMiIgLz4gICAgPGNpcmNsZSAgICAgICByPSIyIiAgICAgICBjeT0iMTA0Ny4zNjIyIiAgICAgICBjeD0iNyIgICAgICAgaWQ9ImNpcmNsZTE3NDAyIiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgLz4gICAgPGNpcmNsZSAgICAgICByPSIyIiAgICAgICBjeT0iMTA0Ny4zNjIyIiAgICAgICBjeD0iMTMiICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MS42MDAwMDAwMjttYXJrZXI6bm9uZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBpZD0iY2lyY2xlMTc0MDQiIC8+ICAgIDxjaXJjbGUgICAgICAgcj0iMiIgICAgICAgY3k9IjEwNDAuNzE0IiAgICAgICBjeD0iNSIgICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjYwMDAwMDAyO21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGlkPSJjaXJjbGUxNzQwNiIgLz4gICAgPGNpcmNsZSAgICAgICByPSIyIiAgICAgICBjeT0iMTA0MC43MTQiICAgICAgIGN4PSIxNSIgICAgICAgaWQ9ImNpcmNsZTE3NDA4IiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjAwMDAwMDI7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgLz4gIDwvZz48L3N2Zz4='); 53 | } 54 | 55 | .mapbox-gl-draw_rectangle { 56 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyMCcgaGVpZ2h0PScyMCc+PHBhdGggZD0nbTE1IDEyLjN2LTQuNmMuNi0uMyAxLTEgMS0xLjcgMC0xLjEtLjktMi0yLTItLjcgMC0xLjQuNC0xLjcgMWgtNC42Yy0uMy0uNi0xLTEtMS43LTEtMS4xIDAtMiAuOS0yIDIgMCAuNy40IDEuNCAxIDEuN3Y0LjZjLS42LjMtMSAxLTEgMS43IDAgMS4xLjkgMiAyIDIgLjcgMCAxLjQtLjQgMS43LTFoNC42Yy4zLjYgMSAxIDEuNyAxIDEuMSAwIDItLjkgMi0yIDAtLjctLjQtMS40LTEtMS43em0tOC0uM3YtNGwxLTFoNGwxIDF2NGwtMSAxaC00eicvPjwvc3ZnPg=='); 57 | } 58 | 59 | .mapbox-gl-draw_circle { 60 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI2LjUuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDIwIDIwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMCAyMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuNTtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cjwvc3R5bGU+CjxnIGlkPSJzdmcxOTE2NyIgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIiB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiPgo8L2c+CjxnIGlkPSJMYXllcl8yIj4KCTxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjEwIiBjeT0iMTAiIHI9IjMuNyIvPgo8L2c+Cjwvc3ZnPgo='); 61 | } 62 | 63 | .mapbox-gl-draw_edit { 64 | background-size: 13px 13px; 65 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgUHJvIDYuMi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlIChDb21tZXJjaWFsIExpY2Vuc2UpIENvcHlyaWdodCAyMDIyIEZvbnRpY29ucywgSW5jLiAtLT48cGF0aCBkPSJNNDcxLjYgMjEuN2MtMjEuOS0yMS45LTU3LjMtMjEuOS03OS4yIDBMMzYyLjMgNTEuN2w5Ny45IDk3LjkgMzAuMS0zMC4xYzIxLjktMjEuOSAyMS45LTU3LjMgMC03OS4yTDQ3MS42IDIxLjd6bS0yOTkuMiAyMjBjLTYuMSA2LjEtMTAuOCAxMy42LTEzLjUgMjEuOWwtMjkuNiA4OC44Yy0yLjkgOC42LS42IDE4LjEgNS44IDI0LjZzMTUuOSA4LjcgMjQuNiA1LjhsODguOC0yOS42YzguMi0yLjggMTUuNy03LjQgMjEuOS0xMy41TDQzNy43IDE3Mi4zIDMzOS43IDc0LjMgMTcyLjQgMjQxLjd6TTk2IDY0QzQzIDY0IDAgMTA3IDAgMTYwVjQxNmMwIDUzIDQzIDk2IDk2IDk2SDM1MmM1MyAwIDk2LTQzIDk2LTk2VjMyMGMwLTE3LjctMTQuMy0zMi0zMi0zMnMtMzIgMTQuMy0zMiAzMnY5NmMwIDE3LjctMTQuMyAzMi0zMiAzMkg5NmMtMTcuNyAwLTMyLTE0LjMtMzItMzJWMTYwYzAtMTcuNyAxNC4zLTMyIDMyLTMyaDk2YzE3LjcgMCAzMi0xNC4zIDMyLTMycy0xNC4zLTMyLTMyLTMySDk2eiIvPjwvc3ZnPg=='); 66 | } 67 | 68 | .save-cancel-control { 69 | background-color: white; 70 | border-radius: 4px; 71 | padding-top: 4px; 72 | padding-bottom: 8px; 73 | padding-left: 8px; 74 | padding-right: 8px; 75 | margin-top: 8px; 76 | margin-right: 8px; 77 | float: right; 78 | clear: both; 79 | pointer-events: auto; 80 | 81 | .label { 82 | font-weight: bold; 83 | margin-bottom: 2px; 84 | } 85 | 86 | .buttons { 87 | display: flex; 88 | 89 | button { 90 | background-color: #6b7280; 91 | font-weight: bold; 92 | color: white; 93 | border-radius: 4px; 94 | padding: 4px 8px; 95 | outline: none; 96 | border: none; 97 | cursor: pointer; 98 | font-size: small; 99 | 100 | &:hover { 101 | background-color: #314151; 102 | } 103 | } 104 | 105 | #mapboxgl-draw-actions-btn_cancel { 106 | margin-left: 4px; 107 | } 108 | } 109 | } 110 | 111 | .hidden { 112 | // display: none; 113 | } 114 | -------------------------------------------------------------------------------- /src/view/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import MapContainer from './components/map-container' 4 | import { StoreProvider, useStore } from './components/store' 5 | import { featureCollection } from '@turf/turf' 6 | import { FeatureCollection } from 'geojson' 7 | import 'mapbox-gl/dist/mapbox-gl.css' 8 | import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' 9 | import './index.less' 10 | import { nanoid } from 'nanoid' 11 | import ProjectionSwitch from './components/projection-switch' 12 | import LayerSwitch from './components/layer-switch' 13 | 14 | function App() { 15 | const { setGeojson } = useStore() 16 | 17 | const handleMessagesFromExtension = useCallback( 18 | (event: MessageEvent) => { 19 | if (event.data === '') { 20 | const empty = featureCollection([]) 21 | setGeojson(empty) 22 | return 23 | } 24 | const json = JSON.parse(event.data) as FeatureCollection 25 | json.features.forEach((feature) => { 26 | feature.properties = { 27 | ...feature.properties, 28 | _id: nanoid() 29 | } 30 | }) 31 | setGeojson(json) 32 | }, 33 | [setGeojson] 34 | ) 35 | 36 | useEffect(() => { 37 | window.addEventListener('message', (event: MessageEvent) => { 38 | handleMessagesFromExtension(event) 39 | }) 40 | return () => { 41 | window.removeEventListener('message', handleMessagesFromExtension) 42 | } 43 | }, [handleMessagesFromExtension]) 44 | 45 | return ( 46 |
47 | 48 | 49 | 50 |
51 | ) 52 | } 53 | 54 | const root = createRoot(document.getElementById('root')!) 55 | root.render( 56 | 57 | 58 | 59 | ) 60 | -------------------------------------------------------------------------------- /src/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ESNext", 5 | "target": "ES2022", 6 | "jsx": "react-jsx", 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "lib": ["ES6", "DOM"], 11 | "rootDir": "./", 12 | "moduleResolution": "bundler" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/view/utils/constant.ts: -------------------------------------------------------------------------------- 1 | import { ProjectionSpecification } from 'mapbox-gl' 2 | import { LayerStyle, Row } from './types' 3 | 4 | export const ACCESS_TOKEN = 5 | 'pk.eyJ1IjoiaW5nZW40MiIsImEiOiJjazlsMnliMXoyMWoxM2tudm1hajRmaHZ6In0.rWx_wAz2cAeMIzxQQfPDPA' 6 | export const DEFAULT_FILL_COLOR = '#555555' 7 | export const DEFAULT_STROKE_COLOR = '#555555' 8 | export const DEFAULT_STROKE_WIDTH = 2 9 | export const DEFAULT_STROKE_OPACITY = 0.8 10 | export const DEFAULT_FILL_OPACITY = 0.5 11 | 12 | export const DEFAULT_FILL_STYLE_ROWS: Row[] = [ 13 | { 14 | field: 'fill', 15 | value: DEFAULT_FILL_COLOR 16 | }, 17 | { 18 | field: 'fill-opacity', 19 | value: DEFAULT_FILL_OPACITY 20 | }, 21 | { 22 | field: 'stroke', 23 | value: DEFAULT_STROKE_COLOR 24 | }, 25 | { 26 | field: 'stroke-width', 27 | value: DEFAULT_STROKE_WIDTH 28 | }, 29 | { 30 | field: 'stroke-opacity', 31 | value: DEFAULT_STROKE_OPACITY 32 | } 33 | ] 34 | 35 | export const DEFAULT_LINE_STYLE_ROWS: Row[] = [ 36 | { 37 | field: 'stroke', 38 | value: DEFAULT_STROKE_COLOR 39 | }, 40 | { 41 | field: 'stroke-width', 42 | value: DEFAULT_STROKE_WIDTH 43 | }, 44 | { 45 | field: 'stroke-opacity', 46 | value: DEFAULT_STROKE_OPACITY 47 | } 48 | ] 49 | 50 | export const DEFAULT_POINT_STYLE_ROWS: Row[] = [ 51 | { 52 | field: 'fill', 53 | value: DEFAULT_FILL_COLOR 54 | }, 55 | { 56 | field: 'fill-opacity', 57 | value: DEFAULT_FILL_OPACITY 58 | }, 59 | { 60 | field: 'stroke', 61 | value: DEFAULT_STROKE_COLOR 62 | }, 63 | { 64 | field: 'stroke-width', 65 | value: DEFAULT_STROKE_WIDTH 66 | }, 67 | { 68 | field: 'stroke-opacity', 69 | value: DEFAULT_STROKE_OPACITY 70 | } 71 | ] 72 | 73 | export const STYLE_FIELDS = [ 74 | 'stroke', 75 | 'stroke-width', 76 | 'stroke-opacity', 77 | 'fill', 78 | 'fill-opacity' 79 | ] 80 | 81 | export const LAYER_STYLES: LayerStyle[] = [ 82 | { 83 | label: 'Standard', 84 | style: 'mapbox://styles/mapbox/standard' 85 | }, 86 | { 87 | label: 'Satellite Streets', 88 | style: 'mapbox://styles/mapbox/satellite-streets-v12' 89 | }, 90 | { 91 | label: 'Outdoors', 92 | style: 'mapbox://styles/mapbox/outdoors-v12' 93 | }, 94 | { 95 | label: 'Light', 96 | style: 'mapbox://styles/mapbox/light-v11' 97 | }, 98 | { 99 | label: 'Dark', 100 | style: 'mapbox://styles/mapbox/dark-v11' 101 | }, 102 | { 103 | label: 'OSM', 104 | style: { 105 | name: 'osm', 106 | version: 8, 107 | glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf', 108 | sources: { 109 | 'osm-raster-tiles': { 110 | type: 'raster', 111 | tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], 112 | tileSize: 256, 113 | attribution: 114 | '© OpenStreetMap' 115 | } 116 | }, 117 | layers: [ 118 | { 119 | id: 'osm-raster-layer', 120 | type: 'raster', 121 | source: 'osm-raster-tiles', 122 | minzoom: 0, 123 | maxzoom: 22 124 | } 125 | ] 126 | } 127 | } 128 | ] 129 | 130 | export const PROJECTIONS: { 131 | label: string 132 | value: ProjectionSpecification['name'] 133 | }[] = [ 134 | { 135 | label: 'Globe', 136 | value: 'globe' 137 | }, 138 | { 139 | label: 'Mercator', 140 | value: 'mercator' 141 | } 142 | ] 143 | -------------------------------------------------------------------------------- /src/view/utils/draw/circle.ts: -------------------------------------------------------------------------------- 1 | // custom mapbopx-gl-draw mode that extends draw_line_string 2 | // shows a center point, radius line, and circle polygon while drawing 3 | 4 | import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw' 5 | import { circle, length } from '@turf/turf' 6 | 7 | // forces draw.create on creation of second vertex 8 | // const circle = require('@turf/circle').default 9 | // const length = require('@turf/length').default 10 | // const MapboxDraw = require('@mapbox/mapbox-gl-draw').default 11 | 12 | // const { getDisplayMeasurements } = require('./util.js') 13 | 14 | function circleFromTwoVertexLineString(geojson: any) { 15 | const center = geojson.geometry.coordinates[0] 16 | const radiusInKm = length(geojson) 17 | 18 | return circle(center, radiusInKm) 19 | } 20 | 21 | const DrawCircle: DrawCustomMode = { 22 | ...MapboxDraw.modes.draw_line_string, 23 | 24 | onClick: function (state: any, e: any) { 25 | // this ends the drawing after the user creates a second point, triggering this.onStop 26 | if (state.currentVertexPosition === 1) { 27 | state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat) 28 | return this.changeMode('simple_select', { featureIds: [state.line.id] }) 29 | } 30 | 31 | state.line.updateCoordinate( 32 | state.currentVertexPosition, 33 | e.lngLat.lng, 34 | e.lngLat.lat 35 | ) 36 | if (state.direction === 'forward') { 37 | state.currentVertexPosition += 1 38 | state.line.updateCoordinate( 39 | state.currentVertexPosition, 40 | e.lngLat.lng, 41 | e.lngLat.lat 42 | ) 43 | } else { 44 | state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat) 45 | } 46 | 47 | return null 48 | }, 49 | 50 | onStop: function (state) { 51 | this.activateUIButton() 52 | 53 | // check to see if we've deleted this feature 54 | if (this.getFeature(state.line.id) === undefined) return 55 | 56 | // remove last added coordinate 57 | state.line.removeCoordinate('0') 58 | if (state.line.isValid()) { 59 | const lineGeoJson = state.line.toGeoJSON() 60 | const circleFeature = circleFromTwoVertexLineString(lineGeoJson) 61 | 62 | this.map.fire('draw.create', { 63 | features: [circleFeature] 64 | }) 65 | } else { 66 | this.deleteFeature(state.line.id, { silent: true }) 67 | this.changeMode('simple_select', {}, { silent: true }) 68 | } 69 | }, 70 | 71 | toDisplayFeatures: function (state, geojson: any, display: any) { 72 | // Only render the line if it has at least one real coordinate 73 | if (geojson.geometry.coordinates.length < 2) return null 74 | 75 | display({ 76 | type: 'Feature', 77 | properties: { 78 | active: 'true' 79 | }, 80 | geometry: { 81 | type: 'Point', 82 | coordinates: geojson.geometry.coordinates[0] 83 | } 84 | }) 85 | 86 | // displays the line as it is drawn 87 | geojson.properties.active = 'true' 88 | display(geojson) 89 | 90 | // const displayMeasurements = getDisplayMeasurements(geojson) 91 | 92 | // create custom feature for the current pointer position 93 | const currentVertex = { 94 | type: 'Feature', 95 | properties: { 96 | meta: 'currentPosition', 97 | // radius: `${displayMeasurements.metric} ${displayMeasurements.standard}`, 98 | parent: state.line.id 99 | }, 100 | geometry: { 101 | type: 'Point', 102 | coordinates: geojson.geometry.coordinates[1] 103 | } 104 | } 105 | 106 | display(currentVertex) 107 | 108 | const circleFeature = circleFromTwoVertexLineString(geojson) 109 | 110 | circleFeature.properties = { 111 | active: 'true' 112 | } 113 | 114 | display(circleFeature) 115 | 116 | return null 117 | } 118 | } 119 | 120 | export default DrawCircle 121 | -------------------------------------------------------------------------------- /src/view/utils/draw/controls.ts: -------------------------------------------------------------------------------- 1 | import { IControl, Map } from 'mapbox-gl' 2 | 3 | export class EditControl implements IControl { 4 | private map: Map | undefined 5 | private container: HTMLElement | undefined 6 | 7 | onAdd(map: Map) { 8 | this.map = map 9 | this.container = document.createElement('div') 10 | this.container.className = 11 | 'mapboxgl-ctrl-group mapboxgl-ctrl edit-control hidden' 12 | 13 | this.container.innerHTML = ` 14 | 16 | ` 17 | 18 | return this.container 19 | } 20 | 21 | onRemove() { 22 | this.container?.parentNode?.removeChild(this.container) 23 | this.map = undefined 24 | } 25 | 26 | open() { 27 | this.container?.style.setProperty('display', 'block') 28 | } 29 | 30 | close() { 31 | this.container?.style.setProperty('display', 'none') 32 | } 33 | 34 | onClick(cb: () => void) { 35 | this.container?.addEventListener('click', cb) 36 | } 37 | } 38 | 39 | export class SaveCancelControl { 40 | private map: mapboxgl.Map | undefined 41 | private container: HTMLElement | undefined 42 | 43 | onAdd(map: mapboxgl.Map) { 44 | this.map = map 45 | this.container = document.createElement('div') 46 | this.container.className = 'save-cancel-control ' 47 | this.container.style.setProperty('display', 'none') 48 | this.container.innerHTML = ` 49 |
Editing Geometries
50 |
51 | 54 | 57 |
58 | ` 59 | 60 | return this.container 61 | } 62 | 63 | onRemove() { 64 | this.container?.parentNode?.removeChild(this.container) 65 | this.map = undefined 66 | } 67 | 68 | open() { 69 | this.container?.style.setProperty('display', 'block') 70 | } 71 | 72 | close() { 73 | this.container?.style.setProperty('display', 'none') 74 | } 75 | 76 | onSaveClick(cb: () => void) { 77 | document 78 | .getElementById('mapboxgl-draw-actions-btn_save') 79 | ?.addEventListener('click', cb) 80 | } 81 | 82 | onCancelClick(cb: () => void) { 83 | document 84 | .getElementById('mapboxgl-draw-actions-btn_cancel') 85 | ?.addEventListener('click', cb) 86 | } 87 | } 88 | 89 | export class TrashControl { 90 | private map: mapboxgl.Map | undefined 91 | private container: HTMLElement | undefined 92 | 93 | onAdd(map: mapboxgl.Map) { 94 | this.map = map 95 | this.container = document.createElement('div') 96 | this.container.className = 'mapboxgl-ctrl-group mapboxgl-ctrl trash-control' 97 | this.container.style.setProperty('display', 'none') 98 | this.container.innerHTML = ` 99 | 101 | ` 102 | return this.container 103 | } 104 | 105 | open() { 106 | this.container?.style.setProperty('display', 'block') 107 | } 108 | 109 | close() { 110 | this.container?.style.setProperty('display', 'none') 111 | } 112 | 113 | onRemove() { 114 | this.container?.parentNode?.removeChild(this.container) 115 | this.map = undefined 116 | } 117 | 118 | onClick(cb: () => void) { 119 | this.container?.addEventListener('click', cb) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/view/utils/draw/extend-draw.ts: -------------------------------------------------------------------------------- 1 | import MapboxDraw from '@mapbox/mapbox-gl-draw' 2 | import { IControl, Map } from 'mapbox-gl' 3 | 4 | type Button = { 5 | on: string 6 | action: (e: any) => void 7 | classes: string[] 8 | title: string 9 | elButton?: HTMLButtonElement 10 | } 11 | 12 | interface Options { 13 | draw: MapboxDraw 14 | buttons: Omit[] 15 | } 16 | 17 | export default class ExtendDraw implements IControl { 18 | private map: Map | undefined 19 | private container: HTMLElement | undefined 20 | private draw: MapboxDraw | undefined 21 | private buttons: Button[] 22 | private onAddOrig: typeof MapboxDraw.prototype.onAdd 23 | private onRemoveOrig: typeof MapboxDraw.prototype.onRemove 24 | 25 | constructor({ draw, buttons }: Options) { 26 | this.draw = draw 27 | this.buttons = buttons 28 | this.onAddOrig = this.draw.onAdd 29 | this.onRemoveOrig = this.draw.onRemove 30 | } 31 | 32 | onAdd(map: Map) { 33 | this.map = map 34 | this.container = this.onAddOrig(map) 35 | this.buttons.forEach((b) => { 36 | this.addButton(b) 37 | }) 38 | return this.container 39 | } 40 | 41 | onRemove(map: Map) { 42 | this.buttons.forEach((b) => { 43 | this.removeButton(b) 44 | }) 45 | this.onRemoveOrig(map) 46 | } 47 | 48 | addButton(opt: Button) { 49 | const elButton = document.createElement('button') 50 | elButton.className = 'mapbox-gl-draw_ctrl-draw-btn' 51 | if (opt.classes instanceof Array) { 52 | opt.classes.forEach((c) => { 53 | elButton.classList.add(c) 54 | }) 55 | } 56 | elButton.addEventListener(opt.on, opt.action) 57 | elButton.title = opt.title 58 | this.container?.appendChild(elButton) 59 | opt.elButton = elButton 60 | } 61 | 62 | removeButton(opt: Button) { 63 | opt.elButton?.removeEventListener(opt.on, opt.action) 64 | opt.elButton?.remove() 65 | } 66 | 67 | open() { 68 | this.container?.style.setProperty('display', 'block') 69 | } 70 | 71 | close() { 72 | this.container?.style.setProperty('display', 'none') 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/view/utils/draw/rectangle.ts: -------------------------------------------------------------------------------- 1 | import { DrawCustomMode } from '@mapbox/mapbox-gl-draw' 2 | 3 | // from https://github.com/thegisdev/mapbox-gl-draw-rectangle-mode 4 | const doubleClickZoom = { 5 | enable: (ctx: any) => { 6 | setTimeout(() => { 7 | // First check we've got a map and some context. 8 | if ( 9 | !ctx.map || 10 | !ctx.map.doubleClickZoom || 11 | !ctx._ctx || 12 | !ctx._ctx.store || 13 | !ctx._ctx.store.getInitialConfigValue 14 | ) 15 | return 16 | // Now check initial state wasn't false (we leave it disabled if so) 17 | if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return 18 | ctx.map.doubleClickZoom.enable() 19 | }, 0) 20 | }, 21 | disable(ctx: any) { 22 | setTimeout(() => { 23 | if (!ctx.map || !ctx.map.doubleClickZoom) return 24 | // Always disable here, as it's necessary in some cases. 25 | ctx.map.doubleClickZoom.disable() 26 | }, 0) 27 | } 28 | } 29 | 30 | const DrawRectangle: DrawCustomMode = { 31 | // When the mode starts this function will be called. 32 | onSetup: function () { 33 | const rectangle = this.newFeature({ 34 | type: 'Feature', 35 | properties: {}, 36 | geometry: { 37 | type: 'Polygon', 38 | coordinates: [[]] 39 | } 40 | }) 41 | this.addFeature(rectangle) 42 | this.clearSelectedFeatures() 43 | doubleClickZoom.disable(this) 44 | this.updateUIClasses({ mouse: 'add' }) 45 | this.setActionableState({ 46 | trash: true, 47 | combineFeatures: false, 48 | uncombineFeatures: false 49 | }) 50 | return { 51 | rectangle 52 | } 53 | }, 54 | // support mobile taps 55 | onTap: function (state, e: any) { 56 | // emulate 'move mouse' to update feature coords 57 | if (state.startPoint && this.onMouseMove && e.type !== 'touchstart') 58 | this.onMouseMove(state, e) 59 | // emulate onClick 60 | this.onClick?.(state, e) 61 | }, 62 | // Whenever a user clicks on the map, Draw will call `onClick` 63 | onClick: function (state, e: any) { 64 | // if state.startPoint exist, means its second click 65 | // change to simple_select mode 66 | if ( 67 | state.startPoint && 68 | state.startPoint[0] !== e.lngLat.lng && 69 | state.startPoint[1] !== e.lngLat.lat 70 | ) { 71 | this.updateUIClasses({ mouse: 'pointer' }) 72 | state.endPoint = [e.lngLat.lng, e.lngLat.lat] 73 | this.changeMode('simple_select', { featuresId: state.rectangle.id }) 74 | } 75 | // on first click, save clicked point coords as starting for rectangle 76 | const startPoint = [e.lngLat.lng, e.lngLat.lat] 77 | state.startPoint = startPoint 78 | }, 79 | onMouseMove: function (state, e) { 80 | // if startPoint, update the feature coordinates, using the bounding box concept 81 | // we are simply using the startingPoint coordinates and the current Mouse Position 82 | // coordinates to calculate the bounding box on the fly, which will be our rectangle 83 | if (state.startPoint) { 84 | state.rectangle.updateCoordinate( 85 | '0.0', 86 | state.startPoint[0], 87 | state.startPoint[1] 88 | ) // minX, minY - the starting point 89 | state.rectangle.updateCoordinate('0.1', e.lngLat.lng, state.startPoint[1]) // maxX, minY 90 | state.rectangle.updateCoordinate('0.2', e.lngLat.lng, e.lngLat.lat) // maxX, maxY 91 | state.rectangle.updateCoordinate('0.3', state.startPoint[0], e.lngLat.lat) // minX,maxY 92 | state.rectangle.updateCoordinate( 93 | '0.4', 94 | state.startPoint[0], 95 | state.startPoint[1] 96 | ) // minX,minY - ending point (equals to starting point) 97 | } 98 | }, 99 | // Whenever a user clicks on a key while focused on the map, it will be sent here 100 | onKeyUp: function (state, e) { 101 | if (e.keyCode === 27) return this.changeMode('simple_select') 102 | }, 103 | onStop: function (state) { 104 | doubleClickZoom.enable(this) 105 | this.updateUIClasses({ mouse: 'none' }) 106 | this.activateUIButton() 107 | 108 | // check to see if we've deleted this feature 109 | if (this.getFeature(state.rectangle.id) === undefined) return 110 | 111 | // remove last added coordinate 112 | state.rectangle.removeCoordinate('0.4') 113 | if (state.rectangle.isValid()) { 114 | this.map.fire('draw.create', { 115 | features: [state.rectangle.toGeoJSON()] 116 | }) 117 | } else { 118 | this.deleteFeature(state.rectangle.id, { silent: true }) 119 | this.changeMode('simple_select', {}, { silent: true }) 120 | } 121 | }, 122 | toDisplayFeatures: function (state, geojson: any, display) { 123 | const isActivePolygon = geojson.properties.id === state.rectangle.id 124 | geojson.properties.active = isActivePolygon ? 'true' : 'false' 125 | if (!isActivePolygon) return display(geojson) 126 | 127 | // Only render the rectangular polygon if it has the starting point 128 | if (!state.startPoint) return 129 | return display(geojson) 130 | }, 131 | onTrash: function (state) { 132 | this.deleteFeature(state.rectangle.id, { silent: true }) 133 | this.changeMode('simple_select') 134 | } 135 | } 136 | 137 | export default DrawRectangle 138 | -------------------------------------------------------------------------------- /src/view/utils/draw/simple-select.ts: -------------------------------------------------------------------------------- 1 | import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw' 2 | 3 | const SimpleSelect: DrawCustomMode = { 4 | ...MapboxDraw.modes.simple_select, 5 | onDrag: function (state, e) { 6 | const selectedFeatures = this.getSelected() 7 | const soloPointSelected = 8 | selectedFeatures.length === 1 && selectedFeatures[0].type === 'Point' 9 | 10 | // if the selected feature is a single point, allow dragging it without holding shift 11 | // shift is required for multiple features, or single linestrings and polygons 12 | if (state.canDragMove && (e.originalEvent.shiftKey || soloPointSelected)) { 13 | // @ts-expect-error this 14 | return this.dragMove(state, e) 15 | } 16 | if (this.drawConfig.boxSelect && state.canBoxSelect) { 17 | // @ts-expect-error this 18 | return this.whileBoxSelect(state, e) 19 | } 20 | } 21 | } 22 | 23 | export default SimpleSelect 24 | -------------------------------------------------------------------------------- /src/view/utils/measure.ts: -------------------------------------------------------------------------------- 1 | import { 2 | round, 3 | area as getArea, 4 | length as getLength, 5 | center as getCenter 6 | } from '@turf/turf' 7 | import { Feature } from 'geojson' 8 | 9 | export default function measure(feature: Feature) { 10 | switch (feature.geometry.type) { 11 | case 'Polygon': 12 | case 'MultiPolygon': { 13 | const areaSqMeters = getArea(feature) 14 | return { 15 | 'Sq. Meters': round(areaSqMeters, 2), 16 | 'Sq. Kilometers': round(areaSqMeters / 1000 / 1000, 2), 17 | Acres: round(areaSqMeters / 666.666666667, 2), 18 | 'Sq. Feet': round(areaSqMeters * 10.7639, 2), 19 | 'Sq. Miles': round(areaSqMeters / 2589988.11034, 2) 20 | } 21 | } 22 | case 'LineString': 23 | case 'MultiLineString': { 24 | const lengthKiloMeters = getLength(feature) 25 | return { 26 | Meters: round(lengthKiloMeters * 1000, 2), 27 | Kilometers: round(lengthKiloMeters, 2), 28 | Feet: round(lengthKiloMeters * 3280.84), 29 | Yards: round(lengthKiloMeters * 1093.61, 2), 30 | Miles: round(lengthKiloMeters * 0.6213712, 2) 31 | } 32 | } 33 | case 'Point': 34 | case 'MultiPoint': { 35 | const center = getCenter(feature.geometry) 36 | return { 37 | Longitude: round(center.geometry.coordinates[0], 4), 38 | Latitude: round(center.geometry.coordinates[1], 4) 39 | } 40 | } 41 | default: 42 | return {} 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/view/utils/styles.ts: -------------------------------------------------------------------------------- 1 | import { LayerSpecification } from 'mapbox-gl' 2 | 3 | const styles: Omit[] = [ 4 | { 5 | id: 'gl-draw-polygon-fill-inactive', 6 | type: 'fill', 7 | filter: [ 8 | 'all', 9 | ['==', 'active', 'false'], 10 | ['==', '$type', 'Polygon'], 11 | ['!=', 'mode', 'static'] 12 | ], 13 | paint: { 14 | 'fill-color': '#3bb2d0', 15 | 'fill-outline-color': '#3bb2d0', 16 | 'fill-opacity': 0.1 17 | } 18 | }, 19 | { 20 | id: 'gl-draw-polygon-fill-active', 21 | type: 'fill', 22 | filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']], 23 | paint: { 24 | 'fill-color': '#fbb03b', 25 | 'fill-outline-color': '#fbb03b', 26 | 'fill-opacity': 0.1 27 | } 28 | }, 29 | { 30 | id: 'gl-draw-polygon-midpoint', 31 | type: 'circle', 32 | filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']], 33 | paint: { 34 | 'circle-radius': 3, 35 | 'circle-color': '#fbb03b' 36 | } 37 | }, 38 | { 39 | id: 'gl-draw-polygon-stroke-inactive', 40 | type: 'line', 41 | filter: [ 42 | 'all', 43 | ['==', 'active', 'false'], 44 | ['==', '$type', 'Polygon'], 45 | ['!=', 'mode', 'static'] 46 | ], 47 | layout: { 48 | 'line-cap': 'round', 49 | 'line-join': 'round' 50 | }, 51 | paint: { 52 | 'line-color': '#3bb2d0', 53 | 'line-width': 2 54 | } 55 | }, 56 | { 57 | id: 'gl-draw-polygon-stroke-active', 58 | type: 'line', 59 | filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']], 60 | layout: { 61 | 'line-cap': 'round', 62 | 'line-join': 'round' 63 | }, 64 | paint: { 65 | 'line-color': '#fbb03b', 66 | 'line-dasharray': [0.2, 2], 67 | 'line-width': 2 68 | } 69 | }, 70 | { 71 | id: 'gl-draw-line-inactive', 72 | type: 'line', 73 | filter: [ 74 | 'all', 75 | ['==', 'active', 'false'], 76 | ['==', '$type', 'LineString'], 77 | ['!=', 'mode', 'static'] 78 | ], 79 | layout: { 80 | 'line-cap': 'round', 81 | 'line-join': 'round' 82 | }, 83 | paint: { 84 | 'line-color': '#3bb2d0', 85 | 'line-width': 2 86 | } 87 | }, 88 | { 89 | id: 'gl-draw-line-active', 90 | type: 'line', 91 | filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']], 92 | layout: { 93 | 'line-cap': 'round', 94 | 'line-join': 'round' 95 | }, 96 | paint: { 97 | 'line-color': '#fbb03b', 98 | 'line-dasharray': [0.2, 2], 99 | 'line-width': 2 100 | } 101 | }, 102 | { 103 | id: 'gl-draw-polygon-and-line-vertex-stroke-inactive', 104 | type: 'circle', 105 | filter: [ 106 | 'all', 107 | ['==', 'meta', 'vertex'], 108 | ['==', '$type', 'Point'], 109 | ['!=', 'mode', 'static'] 110 | ], 111 | paint: { 112 | 'circle-radius': 5, 113 | 'circle-color': '#fff' 114 | } 115 | }, 116 | { 117 | id: 'gl-draw-polygon-and-line-vertex-inactive', 118 | type: 'circle', 119 | filter: [ 120 | 'all', 121 | ['==', 'meta', 'vertex'], 122 | ['==', '$type', 'Point'], 123 | ['!=', 'mode', 'static'] 124 | ], 125 | paint: { 126 | 'circle-radius': 3, 127 | 'circle-color': '#fbb03b' 128 | } 129 | }, 130 | { 131 | id: 'gl-draw-point-point-stroke-inactive', 132 | type: 'circle', 133 | filter: [ 134 | 'all', 135 | ['==', 'active', 'false'], 136 | ['==', '$type', 'Point'], 137 | ['==', 'meta', 'feature'], 138 | ['!=', 'mode', 'static'] 139 | ], 140 | paint: { 141 | 'circle-radius': 5, 142 | 'circle-opacity': 1, 143 | 'circle-color': '#fff' 144 | } 145 | }, 146 | { 147 | id: 'gl-draw-point-inactive', 148 | type: 'circle', 149 | filter: [ 150 | 'all', 151 | ['==', 'active', 'false'], 152 | ['==', '$type', 'Point'], 153 | ['==', 'meta', 'feature'], 154 | ['!=', 'mode', 'static'] 155 | ], 156 | paint: { 157 | 'circle-radius': 3, 158 | 'circle-color': '#3bb2d0' 159 | } 160 | }, 161 | { 162 | id: 'gl-draw-point-stroke-active', 163 | type: 'circle', 164 | filter: [ 165 | 'all', 166 | ['==', '$type', 'Point'], 167 | ['==', 'active', 'true'], 168 | ['!=', 'meta', 'midpoint'] 169 | ], 170 | paint: { 171 | 'circle-radius': 7, 172 | 'circle-color': '#fff' 173 | } 174 | }, 175 | { 176 | id: 'gl-draw-point-active', 177 | type: 'circle', 178 | filter: [ 179 | 'all', 180 | ['==', '$type', 'Point'], 181 | ['!=', 'meta', 'midpoint'], 182 | ['==', 'active', 'true'] 183 | ], 184 | paint: { 185 | 'circle-radius': 5, 186 | 'circle-color': '#fbb03b' 187 | } 188 | }, 189 | { 190 | id: 'gl-draw-polygon-fill-static', 191 | type: 'fill', 192 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 193 | paint: { 194 | 'fill-color': '#404040', 195 | 'fill-outline-color': '#404040', 196 | 'fill-opacity': 0.1 197 | } 198 | }, 199 | { 200 | id: 'gl-draw-polygon-stroke-static', 201 | type: 'line', 202 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']], 203 | layout: { 204 | 'line-cap': 'round', 205 | 'line-join': 'round' 206 | }, 207 | paint: { 208 | 'line-color': '#404040', 209 | 'line-width': 2 210 | } 211 | }, 212 | { 213 | id: 'gl-draw-line-static', 214 | type: 'line', 215 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']], 216 | layout: { 217 | 'line-cap': 'round', 218 | 'line-join': 'round' 219 | }, 220 | paint: { 221 | 'line-color': '#404040', 222 | 'line-width': 2 223 | } 224 | }, 225 | { 226 | id: 'gl-draw-point-static', 227 | type: 'circle', 228 | filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Point']], 229 | paint: { 230 | 'circle-radius': 5, 231 | 'circle-color': '#404040' 232 | } 233 | }, 234 | { 235 | id: 'gl-draw-symbol', 236 | type: 'symbol', 237 | layout: { 238 | 'text-line-height': 1.1, 239 | 'text-size': 15, 240 | 'text-font': ['DIN Pro Medium', 'Arial Unicode MS Regular'], 241 | 'text-anchor': 'left', 242 | 'text-justify': 'left', 243 | 'text-offset': [0.8, 0.8], 244 | 'text-field': ['get', 'radius'], 245 | 'text-max-width': 7 246 | }, 247 | paint: { 248 | 'text-color': 'hsl(0, 0%, 95%)', 249 | 'text-halo-color': 'hsl(0, 5%, 0%)', 250 | 'text-halo-width': 1, 251 | 'text-halo-blur': 1 252 | }, 253 | filter: ['==', 'meta', 'currentPosition'] 254 | } 255 | ] 256 | 257 | export default styles 258 | -------------------------------------------------------------------------------- /src/view/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { MapOptions } from 'mapbox-gl' 2 | 3 | export type Row = { field: string; value: string | number } 4 | 5 | export type LayerStyle = { 6 | label: string 7 | style: Exclude 8 | } 9 | -------------------------------------------------------------------------------- /src/view/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface VsCodeApi { 4 | postMessage: (message: { type: string; data: string }) => void 5 | } 6 | 7 | declare const vscode: VsCodeApi 8 | declare module '@mapbox/mapbox-gl-draw-static-mode' 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "esModuleInterop": true, 5 | "target": "ES2022", 6 | "outDir": "out", 7 | "lib": ["ES2022", "DOM"], 8 | "skipLibCheck": true, 9 | "types": ["node"], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": false 13 | }, 14 | "exclude": ["src/test", "node_modules", "src/view"] 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ext.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | outDir: 'out', 6 | lib: { 7 | entry: '/src/extension.ts', 8 | formats: ['cjs'], 9 | fileName: 'extension' 10 | }, 11 | rollupOptions: { 12 | external: ['vscode', 'path'] 13 | }, 14 | sourcemap: true 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | css: { 7 | preprocessorOptions: { 8 | less: { 9 | javascriptEnabled: true, 10 | modifyVars: {} 11 | } 12 | } 13 | }, 14 | build: { 15 | outDir: 'out/view', 16 | rollupOptions: { 17 | input: '/src/view/index.tsx', 18 | output: { 19 | entryFileNames: 'bundle.js', 20 | assetFileNames: (assetInfo) => { 21 | if (assetInfo.name.endsWith('.css')) { 22 | return 'assets/style.css' 23 | } 24 | return 'assets/[name]-[hash][extname]' 25 | } 26 | } 27 | } 28 | } 29 | }) 30 | --------------------------------------------------------------------------------