├── .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 |
7 |
8 |
9 |
10 |
11 |
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 | 
38 |
39 | 
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 |
7 |
8 |
9 |
10 |
11 |
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 | 
38 |
39 | 
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 | setLayerStyle(s)}>
15 | {s.label}
16 |
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 | setProjection(p.value)}>
15 | {p.label}
16 |
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 |
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 |
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 |
290 | Save
291 |
292 |
293 | Cancel
294 |
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 |
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 |
15 |
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 |
52 | Save
53 |
54 |
55 | Cancel
56 |
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 |
100 |
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 |
--------------------------------------------------------------------------------