├── .github
└── workflows
│ ├── develop.yml
│ └── release.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── demo
├── example.gif
├── index.css
├── index.html
└── index.js
├── package-lock.json
├── package.json
├── rollup.config.mjs
└── src
├── constants.js
├── customDrawStyles.js
├── index.js
└── mode.js
/.github/workflows/develop.yml:
--------------------------------------------------------------------------------
1 | name: Develop
2 | on:
3 | push:
4 | branches:
5 | - develop
6 | jobs:
7 | demo:
8 | name: Demo
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout 🛎️
12 | uses: actions/checkout@v3
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 18
17 | - name: Bundle 📦
18 | run: |
19 | npm ci
20 | npm run build
21 | - name: Install and Build Demo 🔧
22 | working-directory: demo
23 | run: npx vite build --base "/mapbox-gl-draw-split-polygon-mode/"
24 | - name: Deploy 🚀
25 | uses: JamesIves/github-pages-deploy-action@v4
26 | with:
27 | GITHUB_TOKEN: ${{ secrets.GH_ACTIONS }}
28 | BRANCH: gh-pages # The branch the action should deploy to.
29 | FOLDER: demo/dist # The folder the action should deploy.
30 | CLEAN: true # Automatically remove deleted files from the deploy branch
31 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout 🛎️
12 | uses: actions/checkout@v3
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: 18
17 | - name: Bundle 📦
18 | run: |
19 | npm ci
20 | npm run build
21 | - name: Install and Build Demo 🔧
22 | working-directory: demo
23 | run: npx vite build --base "/mapbox-gl-draw-split-polygon-mode/"
24 | - name: Deploy 🚀
25 | uses: JamesIves/github-pages-deploy-action@v4
26 | with:
27 | GITHUB_TOKEN: ${{ secrets.GH_ACTIONS }}
28 | BRANCH: gh-pages # The branch the action should deploy to.
29 | FOLDER: demo/dist # The folder the action should deploy.
30 | CLEAN: true # Automatically remove deleted files from the deploy branch
31 | - name: Publish 📤
32 | uses: JS-DevTools/npm-publish@v1
33 | with:
34 | token: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .local
2 | dist
3 | node_modules
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.enabled": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Reyhane Masumi
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 | [](https://www.npmjs.com/package/mapbox-gl-draw-split-polygon-mode)
2 | 
3 | 
4 |
5 | # mapbox-gl-draw-split-polygon-mode
6 |
7 | A custom mode for [MapboxGL-Draw](https://github.com/mapbox/mapbox-gl-draw) to split polygons based on a drawn lineString.
8 |
9 | > Check [mapbox-gl-draw-split-line-mode](https://github.com/ReyhaneMasumi/mapbox-gl-draw-split-line-mode) For splitting lineStrings.
10 |
11 | ## [DEMO](https://reyhanemasumi.github.io/mapbox-gl-draw-split-polygon-mode/)
12 |
13 | 
14 |
15 | ## Install
16 |
17 | ```bash
18 | npm install mapbox-gl-draw-split-polygon-mode
19 | ```
20 |
21 | or use CDN:
22 |
23 | ```html
24 |
25 | ```
26 |
27 | ## Usage
28 |
29 | ```js
30 | import mapboxGl from "mapbox-gl";
31 | import MapboxDraw from "@mapbox/mapbox-gl-draw";
32 | import defaultDrawStyle from "@mapbox/mapbox-gl-draw/src/lib/theme.js";
33 |
34 | import SplitPolygonMode, {
35 | drawStyles as splitPolygonDrawStyles,
36 | } from "mapbox-gl-draw-split-polygon-mode";
37 |
38 | const map = new mapboxgl.Map({
39 | container: "map",
40 | center: [-91.874, 42.76],
41 | zoom: 12,
42 | });
43 |
44 | draw = new MapboxDraw({
45 | userProperties: true,
46 | displayControlsDefault: false,
47 | modes: {
48 | ...SplitPolygonMode(MapboxDraw.modes),
49 | },
50 | styles: [...splitPolygonDrawStyles(defaultDrawStyle)],
51 | userProperties: true,
52 | });
53 |
54 | map.addControl(draw);
55 |
56 | /// Activate the mode
57 | draw.changeMode("split_polygon");
58 |
59 | /// you can modify the behavior using these options:
60 | draw.changeMode(
61 | "split_polygon",
62 | /** Default option values: */
63 | {
64 | highlightColor: "#222",
65 | lineWidth: 0,
66 | lineWidthUnit: "kilometers",
67 | }
68 | );
69 | ```
70 |
71 | > The syntax used here is because `mapbox-gl-draw-split-polygon-mode` needs to modify the modes object and also the `styles` object passed to the `mapbox-gl-draw`. the reason is this package uses [`mapbox-gl-draw-passing-mode`](https://github.com/mhsattarian/mapbox-gl-draw-passing-mode) underneath (and adds this to modes object) and needs to modify the styles to show the selected feature.
72 |
73 | also, take a look at the [**example**](https://github.com/ReyhaneMasumi/mapbox-gl-draw-split-polygon-mode/blob/main/demo/src/App.js) in the `demo` directory. in this example `mapbox-gl-draw-select-mode` is used so users can select feature after clicking in the split icon in the toolbar and get a highlighting when hover each map feature.
74 |
75 | ### Notes
76 |
77 | Splitting polygons are done using the `polygon-splitter` package. which is pretty neat but has some issues and quirks. if you specify a `lineWidth` option other than `zero (0)` another algorithm is used which doesn't have those issues but creates a spacing between features so they can no longer become `union`.
78 |
79 | Also, There is an issue in `mapbox-gl-draw` which causes multi-features to have the same properties object and therefor if you `uncombine` a multi-feature and try to split one of the pieces the whole multi-feature gets highlighted as the selected feature.
80 |
81 | ### Upgrade from version 1
82 |
83 | ```diff
84 |
85 | import mapboxGl from 'mapbox-gl';
86 | import MapboxDraw from '@mapbox/mapbox-gl-draw';
87 | + import defaultDrawStyle from "https://unpkg.com/@mapbox/mapbox-gl-draw@1.3.0/src/lib/theme.js";
88 |
89 | - import SplitPolygonMode from 'mapbox-gl-draw-split-polygon-mode';
90 | - import mapboxGlDrawPassingMode from 'mapbox-gl-draw-passing-mode';
91 |
92 | + import SplitPolygonMode, {
93 | + drawStyles as splitPolygonDrawStyles,
94 | + } from "mapbox-gl-draw-split-polygon-mode";
95 |
96 |
97 | draw = new MapboxDraw({
98 | - modes: Object.assign(MapboxDraw.modes, {
99 | - splitPolygonMode: SplitPolygonMode,
100 | - passing_mode_line_string: mapboxGlDrawPassingMode(
101 | - MapboxDraw.modes.draw_line_string
102 | - ),
103 | - }),
104 | + modes: {
105 | + ...SplitPolygonMode(MapboxDraw.modes),
106 | + },
107 |
108 | + styles: [...splitPolygonDrawStyles(defaultDrawStyle)],
109 | userProperties: true,
110 | });
111 |
112 | - draw.changeMode('splitPolygonMode');
113 | + draw.changeMode("split_polygon");
114 |
115 | ```
116 |
117 | ## Development
118 |
119 | use the command `npm run dev`. it will take advantage of `vite` to watch, serve, and build the package and the demo.
120 |
121 | ## Acknowledgement
122 |
123 | The main function responsible for cutting the features is from:
124 | https://gis.stackexchange.com/a/344277/145409
125 |
126 | ## License
127 |
128 | MIT © [ReyhaneMasumi](LICENSE)
129 |
--------------------------------------------------------------------------------
/demo/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReyhaneMasumi/mapbox-gl-draw-split-polygon-mode/7882ebee15c2a91ccc75570f17af74850717980a/demo/example.gif
--------------------------------------------------------------------------------
/demo/index.css:
--------------------------------------------------------------------------------
1 | @import "https://unpkg.com/modern-normalize@1.0.0/modern-normalize.css";
2 | @import url("https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css");
3 | @import url("https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.0/mapbox-gl-draw.css");
4 |
5 | html,
6 | body {
7 | width: 100%;
8 | height: 100%;
9 | margin: 0;
10 | }
11 |
12 | #root,
13 | #map {
14 | width: 100%;
15 | height: 100%;
16 | }
17 |
18 | .mapboxgl-ctrl-group .split-polygon {
19 | background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0nVVRGLTgnIHN0YW5kYWxvbmU9J25vJz8+PHN2ZyB4bWxuczpkYz0naHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8nIHhtbG5zOmNjPSdodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMnIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIycgeG1sbnM6c3ZnPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJyB4bWxuczpzb2RpcG9kaT0naHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQnIHhtbG5zOmlua3NjYXBlPSdodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlJyB3aWR0aD0nMjAnIGhlaWdodD0nMjAnIHZpZXdCb3g9JzAgMCAyMCAyMCcgaWQ9J3N2ZzE5MTY3JyB2ZXJzaW9uPScxLjEnIGlua3NjYXBlOnZlcnNpb249JzEuMC4xICgzYmMyZTgxM2Y1LCAyMDIwLTA5LTA3KScgc29kaXBvZGk6ZG9jbmFtZT0nc3BsaXRfcG9seWdvbi5zdmcnPjxkZWZzIGlkPSdkZWZzMTkxNjknPjxtYXJrZXIgc3R5bGU9J292ZXJmbG93OnZpc2libGUnIGlkPSdBcnJvdzFMc3RhcnQnIHJlZlg9JzAuMCcgcmVmWT0nMC4wJyBvcmllbnQ9J2F1dG8nIGlua3NjYXBlOnN0b2NraWQ9J0Fycm93MUxzdGFydCcgaW5rc2NhcGU6aXNzdG9jaz0ndHJ1ZSc+PHBhdGggdHJhbnNmb3JtPSdzY2FsZSgwLjgpIHRyYW5zbGF0ZSgxMi41LDApJyBzdHlsZT0nZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjFwdDtzdHJva2Utb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MScgZD0nTSAwLjAsMC4wIEwgNS4wLC01LjAgTCAtMTIuNSwwLjAgTCA1LjAsNS4wIEwgMC4wLDAuMCB6ICcgaWQ9J3BhdGg4NDknIC8+PC9tYXJrZXI+PC9kZWZzPjxzb2RpcG9kaTpuYW1lZHZpZXcgaWQ9J2Jhc2UnIHBhZ2Vjb2xvcj0nI2ZmZmZmZicgYm9yZGVyY29sb3I9JyM2NjY2NjYnIGJvcmRlcm9wYWNpdHk9JzEuMCcgaW5rc2NhcGU6cGFnZW9wYWNpdHk9JzAuMCcgaW5rc2NhcGU6cGFnZXNoYWRvdz0nMicgaW5rc2NhcGU6em9vbT0nMjAuOTgxMDY4JyBpbmtzY2FwZTpjeD0nOC40MzY4MzkzJyBpbmtzY2FwZTpjeT0nOC4wMjYyMjQ1JyBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ncHgnIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9J2c4NzYnIHNob3dncmlkPSd0cnVlJyB1bml0cz0ncHgnIGlua3NjYXBlOndpbmRvdy13aWR0aD0nMTkyMCcgaW5rc2NhcGU6d2luZG93LWhlaWdodD0nMTAyMScgaW5rc2NhcGU6d2luZG93LXg9JzAnIGlua3NjYXBlOndpbmRvdy15PScwJyBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPScxJyBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9J3RydWUnIGlua3NjYXBlOmRvY3VtZW50LXJvdGF0aW9uPScwJyBpbmtzY2FwZTpjb25uZWN0b3Itc3BhY2luZz0nMyc+PGlua3NjYXBlOmdyaWQgdHlwZT0neHlncmlkJyBpZD0nZ3JpZDE5NzE1JyAvPjwvc29kaXBvZGk6bmFtZWR2aWV3PjxtZXRhZGF0YSBpZD0nbWV0YWRhdGExOTE3Mic+PHJkZjpSREY+PGNjOldvcmsgcmRmOmFib3V0PScnPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlIHJkZjpyZXNvdXJjZT0naHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UnIC8+PGRjOnRpdGxlPjwvZGM6dGl0bGU+PC9jYzpXb3JrPjwvcmRmOlJERj48L21ldGFkYXRhPjxnIGlua3NjYXBlOmxhYmVsPSdMYXllciAxJyBpbmtzY2FwZTpncm91cG1vZGU9J2xheWVyJyBpZD0nbGF5ZXIxJyB0cmFuc2Zvcm09J3RyYW5zbGF0ZSgwLC0xMDMyLjM2MjIpJz48ZyBpZD0nZzg1NCcgdHJhbnNmb3JtPSdtYXRyaXgoMC44OTQwOTk5LDAsMCwwLjg0NTU3MzUsMC4wNjI3NzI5OSwxNTkuODc1ODcpJz48ZyBpZD0nZzg2Mic+PGcgaWQ9J2c4NzYnIHRyYW5zZm9ybT0ndHJhbnNsYXRlKC0wLjM0NjU4MSwtMC40NzgwOTY4NSknPjxnIGlkPSdnODQ1JyB0cmFuc2Zvcm09J21hdHJpeCgxLjM3MDEyNzksMCwwLDEuMzcwMTI3OSwwLjM1OTgwMjgxLC0zODQuMDAyMTQpJz48cGF0aCBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPScwJyBzdHlsZT0nY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjAuNTttYXJrZXI6bm9uZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlJyBkPSdtIDMsMTAzOS4zNjIyIHYgNiBsIDIsMiBoIDYgbCAyLC0yIHYgLTYgbCAtMiwtMiBIIDUgWiBtIDMsMCBoIDQgbCAxLDEgdiA0IGwgLTEsMSBIIDYgbCAtMSwtMSB2IC00IHonIGlkPSdyZWN0Nzc5Nycgc29kaXBvZGk6bm9kZXR5cGVzPSdjY2NjY2NjY2NjY2NjY2NjY2MnIC8+PGNpcmNsZSBzdHlsZT0nY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjttYXJrZXI6bm9uZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlJyBpZD0ncGF0aDQzNjQnIGN4PSc0JyBjeT0nMTA0Ni4zNjIyJyByPScyJyAvPjxjaXJjbGUgaWQ9J3BhdGg0MzY4JyBzdHlsZT0nY29sb3I6IzAwMDAwMDtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuNjttYXJrZXI6bm9uZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlJyBjeD0nMTInIGN5PScxMDQ2LjM2MjInIHI9JzInIC8+PGNpcmNsZSBpZD0ncGF0aDQzNzAnIHN0eWxlPSdjb2xvcjojMDAwMDAwO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MS42O21hcmtlcjpub25lO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUnIGN4PSc0JyBjeT0nMTAzOC4zNjIyJyByPScyJyAvPjxjaXJjbGUgc3R5bGU9J2NvbG9yOiMwMDAwMDA7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjY7bWFya2VyOm5vbmU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZScgaWQ9J3BhdGg0MzcyJyBjeD0nMTInIGN5PScxMDM4LjM2MjInIHI9JzInIC8+PC9nPjxwYXRoIHN0eWxlPSdmaWxsOiNmZmZmZmY7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOiNmZmZmZmY7c3Ryb2tlLXdpZHRoOjEuMTUwMDg4NTI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MC45NDExNzY0NztvcGFjaXR5OjAuOTk4O3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZScgZD0nbSAxMS40NjA4MDYsMTAzNC42NzEzIHYgMTguOTIyJyBpZD0ncGF0aDg0NycgaW5rc2NhcGU6Y29ubmVjdG9yLXR5cGU9J3BvbHlsaW5lJyBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPScwJyAvPjxwYXRoIHN0eWxlPSdvcGFjaXR5OjAuOTk4O2ZpbGw6IzAwMDAwMDtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MC42OTAwNTMxNTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6Mi4wNzAxNTk0NiwwLjY5MDA1MzE1O3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MC45NDExNzY7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZScgZD0nbSAxMS40NjA4MDUsMTAzNC42NzEzIHYgMTguOTIyJyBpZD0ncGF0aDg0Ny0zJyBpbmtzY2FwZTpjb25uZWN0b3ItdHlwZT0ncG9seWxpbmUnIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9JzAnIC8+PC9nPjwvZz48L2c+PC9nPjwvc3ZnPgo=);
20 | }
21 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 | split polygon mode
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import SelectFeatureMode, {
2 | drawStyles as selectFeatureDrawStyles,
3 | } from "mapbox-gl-draw-select-mode";
4 | import defaultDrawStyle from "https://unpkg.com/@mapbox/mapbox-gl-draw@1.3.0/src/lib/theme.js";
5 |
6 | import SplitPolygonMode, {
7 | drawStyles as splitPolygonDrawStyles,
8 | Constants as splitPolygonConstants,
9 | } from "..";
10 |
11 | const { MODE } = import.meta.env;
12 |
13 | import "./index.css";
14 |
15 | let map, draw, drawBar;
16 |
17 | function goSplitMode(selectedFeatureIDs) {
18 | try {
19 | draw?.changeMode("split_polygon", {
20 | featureIds: selectedFeatureIDs,
21 | /** Default option vlaues: */
22 | highlightColor: "#222",
23 | // lineWidth: 0,
24 | // lineWidthUnit: "kilometers",
25 | });
26 | } catch (err) {
27 | console.error(err);
28 | }
29 | }
30 |
31 | function splitPolygon() {
32 | const selectedFeatureIDs = draw.getSelectedIds();
33 |
34 | if (selectedFeatureIDs.length > 0) {
35 | goSplitMode(selectedFeatureIDs);
36 | } else {
37 | draw.changeMode("select_feature", {
38 | selectHighlightColor: "yellow",
39 | onSelect(selectedFeatureID) {
40 | goSplitMode([selectedFeatureID]);
41 | },
42 | });
43 | }
44 | }
45 |
46 | class extendDrawBar {
47 | constructor(opt) {
48 | let ctrl = this;
49 | ctrl.draw = opt.draw;
50 | ctrl.buttons = opt.buttons || [];
51 | ctrl.onAddOrig = opt.draw.onAdd;
52 | ctrl.onRemoveOrig = opt.draw.onRemove;
53 | }
54 | onAdd(map) {
55 | let ctrl = this;
56 | ctrl.map = map;
57 | ctrl.elContainer = ctrl.onAddOrig(map);
58 | ctrl.buttons.forEach((b) => {
59 | ctrl.addButton(b);
60 | });
61 | return ctrl.elContainer;
62 | }
63 | onRemove(map) {
64 | let ctrl = this;
65 | ctrl.buttons.forEach((b) => {
66 | ctrl.removeButton(b);
67 | });
68 | ctrl.onRemoveOrig(map);
69 | }
70 | addButton(opt) {
71 | let ctrl = this;
72 | var elButton = document.createElement("button");
73 | elButton.className = "mapbox-gl-draw_ctrl-draw-btn";
74 | if (opt.classes instanceof Array) {
75 | opt.classes.forEach((c) => {
76 | elButton.classList.add(c);
77 | });
78 | }
79 | elButton.addEventListener(opt.on, opt.action);
80 | ctrl.elContainer.appendChild(elButton);
81 | opt.elButton = elButton;
82 | }
83 | removeButton(opt) {
84 | opt.elButton.removeEventListener(opt.on, opt.action);
85 | opt.elButton.remove();
86 | }
87 | }
88 |
89 | if (mapboxgl.getRTLTextPluginStatus() === "unavailable")
90 | mapboxgl.setRTLTextPlugin(
91 | "https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js",
92 | (err) => {
93 | err && console.error(err);
94 | },
95 | true
96 | );
97 |
98 | map = new mapboxgl.Map({
99 | container: "map",
100 | style:
101 | MODE === "development"
102 | ? { version: 8, sources: {}, layers: [] }
103 | : `https://map.ir/vector/styles/main/mapir-xyz-light-style.json`,
104 | center: [51.3857, 35.6102],
105 | zoom: 7.78,
106 | pitch: 0,
107 | interactive: true,
108 | hash: true,
109 | attributionControl: true,
110 | customAttribution: "© Map © Openstreetmap",
111 | transformRequest: (url) => {
112 | return {
113 | url: url,
114 | headers: {
115 | "x-api-key":
116 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImRiZWU0YWU4OTk4OTA3MmQ3OTFmMjQ4ZDE5N2VhZTgwZWU2NTUyYjhlYjczOWI2NDdlY2YyYzIzNWRiYThiMzIzOTM5MDkzZDM0NTY2MmU3In0.eyJhdWQiOiI5NDMyIiwianRpIjoiZGJlZTRhZTg5OTg5MDcyZDc5MWYyNDhkMTk3ZWFlODBlZTY1NTJiOGViNzM5YjY0N2VjZjJjMjM1ZGJhOGIzMjM5MzkwOTNkMzQ1NjYyZTciLCJpYXQiOjE1OTA4MjU0NzIsIm5iZiI6MTU5MDgyNTQ3MiwiZXhwIjoxNTkzNDE3NDcyLCJzdWIiOiIiLCJzY29wZXMiOlsiYmFzaWMiXX0.M_z4xJlJRuYrh8RFe9UrW89Y_XBzpPth4yk3hlT-goBm8o3x8DGCrSqgskFfmJTUD2wC2qSoVZzQKB67sm-swtD5fkxZO7C0lBCMAU92IYZwCdYehIOtZbP5L1Lfg3C6pxd0r7gQOdzcAZj9TStnKBQPK3jSvzkiHIQhb6I0sViOS_8JceSNs9ZlVelQ3gs77xM2ksWDM6vmqIndzsS-5hUd-9qdRDTLHnhdbS4_UBwNDza47Iqd5vZkBgmQ_oDZ7dVyBuMHiQFg28V6zhtsf3fijP0UhePCj4GM89g3tzYBOmuapVBobbX395FWpnNC3bYg7zDaVHcllSUYDjGc1A", //dev api key
117 | "Mapir-SDK": "reactjs",
118 | },
119 | };
120 | },
121 | });
122 |
123 | draw = new MapboxDraw({
124 | modes: {
125 | ...SplitPolygonMode(SelectFeatureMode(MapboxDraw.modes)),
126 | },
127 | styles: [
128 | ...splitPolygonDrawStyles(selectFeatureDrawStyles(defaultDrawStyle)),
129 | ],
130 | userProperties: true,
131 | });
132 |
133 | window.draw = draw;
134 |
135 | drawBar = new extendDrawBar({
136 | draw: draw,
137 | buttons: [
138 | {
139 | on: "click",
140 | action: splitPolygon,
141 | classes: ["split-polygon"],
142 | },
143 | ],
144 | });
145 |
146 | map.once("load", () => {
147 | map.resize();
148 | map.addControl(drawBar, "top-right");
149 | draw.set({
150 | type: "FeatureCollection",
151 | features: [
152 | {
153 | id: "example",
154 | type: "Feature",
155 | properties: {},
156 | geometry: {
157 | coordinates: [
158 | [
159 | [
160 | [52, 35],
161 | [53, 35],
162 | [53, 36],
163 | [52, 36],
164 | [52, 35],
165 | ],
166 | ],
167 | [
168 | [
169 | [50, 35],
170 | [51, 35],
171 | [51, 36],
172 | [50, 36],
173 | [50, 35],
174 | ],
175 | [
176 | [50.2, 35.2],
177 | [50.8, 35.2],
178 | [50.8, 35.8],
179 | [50.2, 35.8],
180 | [50.2, 35.2],
181 | ],
182 | ],
183 | ],
184 | type: "MultiPolygon",
185 | },
186 | },
187 | ],
188 | });
189 |
190 | map.on("draw.update", function (e) {
191 | console.log("🚀 ~ file: index.js ~ line 158 ~ e", e);
192 |
193 | /// Fixing an issue caused by mapbox-gl-draw. check `Readme.md` section ##Notes.
194 | if (e.action === "split_polygon") {
195 | const allFeatures = draw.getAll().features;
196 |
197 | allFeatures.forEach(({ id }) =>
198 | draw.setFeatureProperty(
199 | id,
200 | splitPolygonConstants.highlightPropertyName,
201 | undefined
202 | )
203 | );
204 | }
205 | });
206 | });
207 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mapbox-gl-draw-split-polygon-mode",
3 | "version": "2.2.1",
4 | "description": "A custom mode for MapboxGL Draw to split polygons",
5 | "main": "dist/index.js",
6 | "module": "src/index.js",
7 | "scripts": {
8 | "dev": "npx vite serve demo --host",
9 | "build": "rollup -c"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/ReyhaneMasumi/mapbox-gl-draw-split-polygon-mode.git"
14 | },
15 | "keywords": [
16 | "mapbox",
17 | "mapbox-gl",
18 | "mapbox-gl-draw",
19 | "geojson"
20 | ],
21 | "author": "Reyhane Masumi",
22 | "contributors": [
23 | "Mohammad H. Sattarian"
24 | ],
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/ReyhaneMasumi/mapbox-gl-draw-split-polygon-mode/issues"
28 | },
29 | "homepage": "https://github.com/ReyhaneMasumi/mapbox-gl-draw-split-polygon-mode#readme",
30 | "files": [
31 | "dist",
32 | "src"
33 | ],
34 | "devDependencies": {
35 | "@rollup/plugin-commonjs": "23.0.2",
36 | "@rollup/plugin-inject": "5.0.2",
37 | "@rollup/plugin-node-resolve": "15.0.1",
38 | "@rollup/plugin-terser": "^0.1.0",
39 | "eslint": "8.26.0",
40 | "rollup": "^3.2.5",
41 | "mapbox-gl-draw-select-mode": "^1.0.0"
42 | },
43 | "peerDependencies": {
44 | "@mapbox/mapbox-gl-draw": "^1.3.0",
45 | "mapbox-gl-draw-passing-mode": "^2.1.0"
46 | },
47 | "dependencies": {
48 | "@turf/turf": "6.5.0",
49 | "polygon-splitter": "^0.0.11"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import resolve from "@rollup/plugin-node-resolve";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import terser from "@rollup/plugin-terser";
4 | import pkg from "./package.json" assert { type: "json" };
5 |
6 | export default {
7 | input: "src/index.js",
8 | plugins: [resolve(), commonjs(), terser()],
9 | output: {
10 | file: pkg.main,
11 | format: "umd",
12 | exports: "named",
13 | name: "SplitPolygonMode",
14 | sourcemap: process.env.NODE_ENV !== "production",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const modeName = "split_polygon";
2 |
3 | /// This mode uses the `mapbox-gl-draw-passing-mode` mode to draw the spilitting lineString.
4 | /// here is the name used to add that mode:
5 | export const passingModeName = `${modeName}_passing_draw_line_string`;
6 |
7 | /// when a (multi-)polygon feature is selected to be splitted, it gets highlighted.
8 | /// here is the name of the property indicating the highlight.
9 | export const highlightPropertyName = `${modeName}_highlight`;
10 |
11 | export const defaultOptions = {
12 | highlightColor: "#222",
13 | lineWidth: 0,
14 | lineWidthUnit: "kilometers",
15 | onSelectFeatureRequest() {
16 | throw new Error("no Feature is selected to split.");
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/customDrawStyles.js:
--------------------------------------------------------------------------------
1 | import {
2 | modeName,
3 | highlightPropertyName as _highlightPropertyName,
4 | } from "./constants";
5 |
6 | const highlightPropertyName = `user_${_highlightPropertyName}`;
7 |
8 | const customDrawStyles = (defaultStyle) =>
9 | defaultStyle
10 | .map((style) => {
11 | if (style.id.endsWith("inactive")) {
12 | return {
13 | ...style,
14 | /// here "!has" is used cause the gl-draw supported that instead of ['!', ['has', ...]]
15 | filter: [...style.filter, ["!has", highlightPropertyName]],
16 | };
17 | }
18 |
19 | return style;
20 | })
21 | .concat([
22 | {
23 | id: `${modeName}-fill-active`,
24 | type: "fill",
25 | filter: [
26 | "all",
27 | ["==", "active", "false"],
28 | ["==", "$type", "Polygon"],
29 | ["has", highlightPropertyName],
30 | ],
31 | paint: {
32 | "fill-color": ["get", highlightPropertyName],
33 | "fill-outline-color": ["get", highlightPropertyName],
34 | "fill-opacity": 0.1,
35 | },
36 | },
37 | {
38 | id: `${modeName}-stroke-active`,
39 | type: "line",
40 | filter: [
41 | "all",
42 | ["==", "active", "false"],
43 | ["==", "$type", "Polygon"],
44 | ["has", highlightPropertyName],
45 | ],
46 | layout: {
47 | "line-cap": "round",
48 | "line-join": "round",
49 | },
50 | paint: {
51 | "line-color": ["get", highlightPropertyName],
52 | "line-dasharray": [0.2, 2],
53 | "line-width": 2,
54 | },
55 | },
56 | ]);
57 |
58 | export default customDrawStyles;
59 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { default as splitPolygonMode } from "./mode.js";
2 | import { default as drawStyles } from "./customDrawStyles.js";
3 | import * as Constants from "./constants";
4 |
5 | import { passing_draw_line_string } from "mapbox-gl-draw-passing-mode";
6 | import SelectFeatureMode from "mapbox-gl-draw-select-mode";
7 | import { modeName, passingModeName } from "./constants";
8 |
9 | export { splitPolygonMode };
10 | export { drawStyles };
11 | export { Constants };
12 |
13 | export default function SplitPolygonMode(modes) {
14 | return {
15 | ...SelectFeatureMode(modes),
16 | [passingModeName]: passing_draw_line_string,
17 | [modeName]: splitPolygonMode,
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/mode.js:
--------------------------------------------------------------------------------
1 | import polygonSplitter from "polygon-splitter";
2 |
3 | import { geojsonTypes, events } from "@mapbox/mapbox-gl-draw/src/constants";
4 |
5 | import lineIntersect from "@turf/line-intersect";
6 | import booleanDisjoint from "@turf/boolean-disjoint";
7 | import lineOffset from "@turf/line-offset";
8 | import lineToPolygon from "@turf/line-to-polygon";
9 | import difference from "@turf/difference";
10 | import { lineString } from "@turf/helpers";
11 |
12 | import {
13 | modeName,
14 | passingModeName,
15 | highlightPropertyName,
16 | defaultOptions,
17 | } from "./constants";
18 |
19 | const SplitPolygonMode = {};
20 |
21 | SplitPolygonMode.onSetup = function (opt) {
22 | const {
23 | featureIds = [],
24 | highlightColor = defaultOptions.highlightColor,
25 | lineWidth = defaultOptions.lineWidth,
26 | lineWidthUnit = defaultOptions.lineWidthUnit,
27 | onSelectFeatureRequest = defaultOptions.onSelectFeatureRequest,
28 | } = opt || {};
29 |
30 | const api = this._ctx.api;
31 |
32 | const featuresToSplit = [];
33 | const selectedFeatures = this.getSelected();
34 |
35 | if (featureIds.length !== 0) {
36 | featuresToSplit.push.apply(
37 | featuresToSplit,
38 | featureIds.map((id) => api.get(id))
39 | );
40 | } else if (selectedFeatures.length !== 0) {
41 | featuresToSplit.push.apply(
42 | featuresToSplit,
43 | selectedFeatures
44 | .filter(
45 | (f) =>
46 | f.type === geojsonTypes.POLYGON ||
47 | f.type === geojsonTypes.MULTI_POLYGON
48 | )
49 | .map((f) => f.toGeoJSON())
50 | );
51 | } else {
52 | return onSelectFeatureRequest();
53 | }
54 |
55 | const state = {
56 | options: {
57 | highlightColor,
58 | lineWidth,
59 | lineWidthUnit,
60 | },
61 | featuresToSplit,
62 | api,
63 | };
64 |
65 | /// `onSetup` job should complete for this mode to work.
66 | /// so `setTimeout` is used to bupass mode change after `onSetup` is done executing.
67 | setTimeout(this.drawAndSplit.bind(this, state), 0);
68 | this.highlighFeatures(state);
69 |
70 | return state;
71 | };
72 |
73 | SplitPolygonMode.drawAndSplit = function (state) {
74 | const { api, options } = state;
75 | const { lineWidth, lineWidthUnit } = options;
76 |
77 | try {
78 | this.changeMode(passingModeName, {
79 | onDraw: (cuttingLineString) => {
80 | const newPolygons = [];
81 | state.featuresToSplit.forEach((el) => {
82 | if (booleanDisjoint(el, cuttingLineString)) {
83 | console.info(`Line was outside of Polygon ${el.id}`);
84 | newPolygons.push(el);
85 | return;
86 | } else if (lineWidth === 0) {
87 | const polycut = polygonCut(el.geometry, cuttingLineString.geometry);
88 | polycut.id = el.id;
89 | api.add(polycut);
90 | newPolygons.push(polycut);
91 | } else {
92 | const polycut = polygonCutWithSpacing(
93 | el.geometry,
94 | cuttingLineString.geometry,
95 | {
96 | line_width: lineWidth,
97 | line_width_unit: lineWidthUnit,
98 | }
99 | );
100 | polycut.id = el.id;
101 | api.add(polycut);
102 | newPolygons.push(polycut);
103 | }
104 | });
105 |
106 | this.fireUpdate(newPolygons);
107 | this.highlighFeatures(state, false);
108 | },
109 | onCancel: () => {
110 | this.highlighFeatures(state, false);
111 | },
112 | });
113 | } catch (err) {
114 | console.error("🚀 ~ file: mode.js ~ line 116 ~ err", err);
115 | }
116 | };
117 |
118 | SplitPolygonMode.highlighFeatures = function (state, shouldHighlight = true) {
119 | const color = shouldHighlight ? state.options.highlightColor : undefined;
120 |
121 | state.featuresToSplit.forEach((f) => {
122 | state.api.setFeatureProperty(f.id, highlightPropertyName, color);
123 | });
124 | };
125 |
126 | SplitPolygonMode.toDisplayFeatures = function (state, geojson, display) {
127 | display(geojson);
128 | };
129 |
130 | SplitPolygonMode.fireUpdate = function (newF) {
131 | this.map.fire(events.UPDATE, {
132 | action: modeName,
133 | features: newF,
134 | });
135 | };
136 |
137 | // SplitPolygonMode.onStop = function ({ main }) {
138 | // console.log("🚀 ~ file: mode.js ~ line 60 ~ onStop");
139 | // };
140 |
141 | export default SplitPolygonMode;
142 |
143 | /// Note: currently has some issues, but generally is a better approach
144 | function polygonCut(poly, line) {
145 | return polygonSplitter(poly, line);
146 | }
147 |
148 | /// Adopted from https://gis.stackexchange.com/a/344277/145409
149 | function polygonCutWithSpacing(poly, line, options) {
150 | const { line_width, line_width_unit } = options || {};
151 |
152 | const offsetLine = [];
153 | const retVal = null;
154 | let i, j, intersectPoints, forCut, forSelect;
155 | let thickLineString, thickLinePolygon, clipped;
156 |
157 | if (
158 | typeof line_width === "undefined" ||
159 | typeof line_width_unit === "undefined" ||
160 | (poly.type != geojsonTypes.POLYGON &&
161 | poly.type != geojsonTypes.MULTI_POLYGON) ||
162 | line.type != geojsonTypes.LINE_STRING
163 | ) {
164 | return retVal;
165 | }
166 |
167 | /// if line and polygon don't intersect return.
168 | if (booleanDisjoint(line, poly)) {
169 | return retVal;
170 | }
171 |
172 | intersectPoints = lineIntersect(poly, line);
173 | if (intersectPoints.features.length === 0) {
174 | return retVal;
175 | }
176 |
177 | /// Creating two new lines at sides of the splitting lineString
178 | offsetLine[0] = lineOffset(line, line_width, {
179 | units: line_width_unit,
180 | });
181 | offsetLine[1] = lineOffset(line, -line_width, {
182 | units: line_width_unit,
183 | });
184 |
185 | for (i = 0; i <= 1; i++) {
186 | forCut = i;
187 | forSelect = (i + 1) % 2;
188 | const polyCoords = [];
189 | for (j = 0; j < line.coordinates.length; j++) {
190 | polyCoords.push(line.coordinates[j]);
191 | }
192 | for (j = offsetLine[forCut].geometry.coordinates.length - 1; j >= 0; j--) {
193 | polyCoords.push(offsetLine[forCut].geometry.coordinates[j]);
194 | }
195 | polyCoords.push(line.coordinates[0]);
196 |
197 | thickLineString = lineString(polyCoords);
198 | thickLinePolygon = lineToPolygon(thickLineString);
199 | clipped = difference(poly, thickLinePolygon);
200 | }
201 |
202 | return clipped;
203 | }
204 |
--------------------------------------------------------------------------------