├── .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 | [![NPM](https://img.shields.io/npm/v/mapbox-gl-draw-split-polygon-mode.svg)](https://www.npmjs.com/package/mapbox-gl-draw-split-polygon-mode) 2 | ![Develop](https://github.com/reyhanemasumi/mapbox-gl-draw-split-polygon-mode/workflows/Develop/badge.svg) 3 | ![Release](https://github.com/reyhanemasumi/mapbox-gl-draw-split-polygon-mode/workflows/Release/badge.svg) 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 | ![A GIF showing how to split a polygon](demo/example.gif) 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 |
15 |
16 |
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 | --------------------------------------------------------------------------------