├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── lint-and-build.yml │ ├── publish-prerelease.yml │ └── publish-release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── Dockerfile ├── LICENSE ├── README.md ├── example └── index.js ├── index.html ├── lib └── main.js ├── package-lock.json ├── package.json └── prepare-release.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [{package.json,package-lock.json,*.yml}] 14 | indent_size = 2 15 | 16 | [README.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # /node_modules/* and /bower_components/* ignored by default 2 | 3 | # Ignore built files 4 | build/* 5 | 6 | # Ignore dist files 7 | dist/* 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "google"], 3 | "env": { 4 | "browser": true, 5 | "commonjs": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "max-len": ["error", {"code": 120, "ignoreStrings": true}], 13 | "comma-dangle": ["error", "never"], 14 | "space-before-function-paren": ["warn", {"anonymous": "always", "named": "never"}] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mblomdahl 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | ## 🐛 Describe the bug 7 | 8 | 9 | 10 | ## ⚠️ Current behavior 11 | 12 | 13 | 14 | ## ✅ Expected behavior 15 | 16 | 17 | 18 | ## 💣 Steps to reproduce 19 | 20 | 21 | 22 | ## 📷 Screenshots 23 | 24 | 25 | 26 | ## 📱 Tech info 27 | 28 | - Device: 29 | - OS: 30 | - Library/App version: 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## ⚠️ Is your feature request related to a problem? Please describe 7 | 8 | 9 | 10 | ## 💡 Describe the solution you'd like 11 | 12 | 13 | 14 | ## 🤚 Do you want to develop this feature yourself? 15 | 16 | 17 | 18 | - [ ] Yes 19 | - [ ] No 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 🚀 Description 4 | 5 | 6 | 7 | ## 📄 Motivation and Context 8 | 9 | 10 | 11 | 12 | ## 🧪 How Has This Been Tested? 13 | 14 | 15 | 16 | 17 | 18 | ## 📷 Screenshots (if appropriate) 19 | 20 | 21 | 22 | ## 📦 Types of changes 23 | 24 | 25 | 26 | - [ ] Bug fix (non-breaking change which fixes an issue) 27 | - [ ] New feature (non-breaking change which adds functionality) 28 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 29 | 30 | ## ✅ Checklist 31 | 32 | 33 | 34 | 35 | - [ ] My code follows the code style of this project. 36 | - [ ] My change requires a change to the documentation. 37 | - [ ] I have updated the documentation accordingly. 38 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-build.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint-and-build: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | steps: 10 | # - uses: hmarr/debug-action@v2 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - run: npm ci 16 | - run: npm run lint 17 | - run: npm run browserify 18 | - run: npm run prepare 19 | - run: npm run docs 20 | -------------------------------------------------------------------------------- /.github/workflows/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish Pre-Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Lint and Build 7 | types: 8 | - completed 9 | branches: [master] 10 | 11 | jobs: 12 | publish-prerelease: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == 'smithmicro/mapbox-gl-circle' }} 15 | permissions: 16 | packages: write 17 | contents: write 18 | steps: 19 | # - uses: hmarr/debug-action@v2 20 | - uses: actions/checkout@v3 21 | with: 22 | persist-credentials: false 23 | fetch-depth: 0 24 | ref: master 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | - run: npm ci 29 | - name: Publish Pre-Release 30 | run: | 31 | # Versioning 32 | PKG_VERSION=$(npm version prerelease) 33 | export BUILD_VERSION=${PKG_VERSION/v/} 34 | CHANGELOG_SECTION="### v. $(echo $BUILD_VERSION | cut -d- -f1)" 35 | echo "Ensure changelog has a '$CHANGELOG_SECTION' section ..." && grep -Fq "$CHANGELOG_SECTION" README.md 36 | 37 | # Build API documentation 38 | npm run docs 39 | 40 | # Build and publish to GHP 41 | npm pkg set name=@smithmicro/mapbox-gl-circle 42 | npm install 43 | rm -rf dist/ 44 | npm run browserify 45 | npm run prepare 46 | echo 'registry=https://npm.pkg.github.com' >> .npmrc 47 | echo '//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}' >> .npmrc 48 | npm publish --tag=next --access=public 49 | git restore .npmrc 50 | echo "Pre-release @smithmicro/mapbox-gl-circle-$BUILD_VERSION published to GHP." 51 | 52 | # Build and publish to NPM 53 | npm pkg set name=mapbox-gl-circle 54 | npm install 55 | rm -rf dist/ 56 | npm run browserify 57 | npm run prepare 58 | echo 'registry=https://registry.npmjs.org' >> .npmrc 59 | echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}' >> .npmrc 60 | npm publish --tag=next --access=public 61 | git restore .npmrc 62 | echo "Pre-release mapbox-gl-circle-$BUILD_VERSION published to NPM." 63 | 64 | git config --local user.email "actions@github.com" 65 | git config --local user.name "GitHub Actions" 66 | git add README.md package.json package-lock.json 67 | git status 68 | git commit -m":package: [skip ci] Pre-release \`$BUILD_VERSION\` by :robot: 69 | 70 | Co-authored-by: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 71 | " 72 | git log -n 2 73 | echo "Done." && exit 0 74 | - name: Push Version Update 75 | uses: ad-m/github-push-action@master 76 | with: 77 | branch: master 78 | github_token: ${{ secrets.GHA_MAPBOXGLCIRCLE_GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-release: 9 | runs-on: ubuntu-latest 10 | if: ${{ contains(github.event.repository.url, 'smithmicro/mapbox-gl-circle') }} 11 | permissions: 12 | packages: write 13 | contents: write 14 | steps: 15 | # - uses: hmarr/debug-action@v2 16 | - uses: actions/checkout@v3 17 | with: 18 | persist-credentials: false 19 | fetch-depth: 0 20 | ref: master 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - run: npm ci 25 | - name: Publish Release ${{ github.event.release.name }} 26 | run: | 27 | # Versioning 28 | PKG_VERSION=$(npm version from-git) 29 | export BUILD_VERSION=${PKG_VERSION/v/} 30 | CHANGELOG_SECTION="### v. $BUILD_VERSION" 31 | echo "Ensure changelog has a '$CHANGELOG_SECTION' section ..." && grep -Fq "$CHANGELOG_SECTION" README.md 32 | 33 | # Build API documentation 34 | npm run docs 35 | 36 | # Build and publish to GHP 37 | npm pkg set name=@smithmicro/mapbox-gl-circle 38 | npm install 39 | rm -rf dist/ 40 | npm run browserify 41 | npm run prepare 42 | echo 'registry=https://npm.pkg.github.com' >> .npmrc 43 | echo '//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}' >> .npmrc 44 | npm publish --tag=latest --access=public 45 | git restore .npmrc 46 | echo "Release @smithmicro/mapbox-gl-circle-$BUILD_VERSION published to GHP." 47 | 48 | # Build and publish to NPM 49 | npm pkg set name=mapbox-gl-circle 50 | npm install 51 | rm -rf dist/ 52 | npm run browserify 53 | npm run prepare 54 | echo 'registry=https://registry.npmjs.org' >> .npmrc 55 | echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}' >> .npmrc 56 | npm publish --tag=latest --access=public 57 | git restore .npmrc 58 | echo "Release mapbox-gl-circle-$BUILD_VERSION published to NPM." 59 | 60 | git config --local user.email "actions@github.com" 61 | git config --local user.name "GitHub Actions" 62 | git add README.md package.json package-lock.json 63 | git status 64 | git commit -m":package: [skip ci] Release \`$BUILD_VERSION\` by :robot: 65 | 66 | Co-authored-by: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 67 | " 68 | git log -n 2 69 | echo "Done." && exit 0 70 | - name: Push Version Update 71 | uses: ad-m/github-push-action@master 72 | with: 73 | branch: master 74 | github_token: ${{ secrets.GHA_MAPBOXGLCIRCLE_GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | dist/ 4 | *.tgz 5 | *.min.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _docker-build/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version=false 2 | allow-same-version=true 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build: docker build -t mapbox-gl-circle:dev . 2 | # Start: docker run -itp 9966:9966 mapbox-gl-circle:dev 3 | # Evaluate: open http://localhost:9966 4 | 5 | FROM node:16-alpine 6 | 7 | WORKDIR /opt/mapbox-gl-circle 8 | 9 | COPY package.json /opt/mapbox-gl-circle/ 10 | 11 | RUN npm install && mkdir -p example/ lib/ 12 | 13 | COPY lib/main.js /opt/mapbox-gl-circle/lib/ 14 | COPY example/index.js /opt/mapbox-gl-circle/example/ 15 | 16 | EXPOSE 9966 35729 17 | 18 | CMD npm start 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Smith Micro Software, Inc. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spherical-Cap "Native Circle" for Mapbox GL JS 2 | 3 | [![Lint and Build](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/lint-and-build.yml/badge.svg)](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/lint-and-build.yml) 4 | [![Publish Pre-Release](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-prerelease.yml/badge.svg)](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-prerelease.yml) 5 | [![Publish Release](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-release.yml/badge.svg)](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-release.yml) 6 | [![NPM Version](https://img.shields.io/npm/v/mapbox-gl-circle.svg)](https://www.npmjs.com/package/mapbox-gl-circle) 7 | 8 | This project uses Turf.js to create a `google.maps.Circle` replacement, as a Mapbox GL JS compatible GeoJSON object. 9 | Allowing the developer to define a circle using center coordinates and radius (in meters). And, optionally, enabling 10 | interactive editing via draggable center/radius handles. Just like the Google original! 11 | 12 | ## Getting Started 13 | 14 | Include [mapbox-gl-circle.min.js](https://npmcdn.com/mapbox-gl-circle/dist/mapbox-gl-circle.min.js) in 15 | the `` of your HTML file to add the _MapboxCircle_ object to global scope: 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | Or even better, fashionably importing it using a module bundler: 22 | 23 | ```npm 24 | npm install --save mapbox-gl-circle 25 | ``` 26 | 27 | ```javascript 28 | const MapboxCircle = require('mapbox-gl-circle'); 29 | // or "import MapboxCircle from 'mapbox-gl-circle';" 30 | ``` 31 | 32 | ## Usage 33 | 34 | 35 | 36 | ### MapboxCircle 37 | 38 | A `google.maps.Circle` replacement for Mapbox GL JS, rendering a "spherical cap" on top of the world. 39 | 40 | **Parameters** 41 | 42 | - `center` 43 | - `radius` 44 | - `options` 45 | 46 | **Examples** 47 | 48 | ```javascript 49 | var myCircle = new MapboxCircle({lat: 39.984, lng: -75.343}, 25000, { 50 | editable: true, 51 | minRadius: 1500, 52 | fillColor: '#29AB87' 53 | }).addTo(myMapboxGlMap); 54 | 55 | myCircle.on('centerchanged', function (circleObj) { 56 | console.log('New center:', circleObj.getCenter()); 57 | }); 58 | myCircle.once('radiuschanged', function (circleObj) { 59 | console.log('New radius (once!):', circleObj.getRadius()); 60 | }); 61 | myCircle.on('click', function (mapMouseEvent) { 62 | console.log('Click:', mapMouseEvent.point); 63 | }); 64 | myCircle.on('contextmenu', function (mapMouseEvent) { 65 | console.log('Right-click:', mapMouseEvent.lngLat); 66 | }); 67 | ``` 68 | 69 | #### constructor 70 | 71 | **Parameters** 72 | 73 | - `center` **({lat: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), lng: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)} | \[[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)])** Circle center as an object or `[lng, lat]` coordinates 74 | - `radius` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Meter radius 75 | - `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 76 | - `options.editable` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** Enable handles for changing center and radius (optional, default `false`) 77 | - `options.minRadius` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Minimum radius on user interaction (optional, default `10`) 78 | - `options.maxRadius` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Maximum radius on user interaction (optional, default `1100000`) 79 | - `options.strokeColor` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Stroke color (optional, default `'#000000'`) 80 | - `options.strokeWeight` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Stroke weight (optional, default `0.5`) 81 | - `options.strokeOpacity` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Stroke opacity (optional, default `0.75`) 82 | - `options.fillColor` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Fill color (optional, default `'#FB6A4A'`) 83 | - `options.fillOpacity` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Fill opacity (optional, default `0.25`) 84 | - `options.refineStroke` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** Adjust circle polygon precision based on radius and zoom 85 | (i.e. prettier circles at the expense of performance) (optional, default `false`) 86 | - `options.properties` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Property metadata for Mapbox GL JS circle object (optional, default `{}`) 87 | 88 | #### on 89 | 90 | Subscribe to circle event. 91 | 92 | **Parameters** 93 | 94 | - `event` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Event name; `click`, `contextmenu`, `centerchanged` or `radiuschanged` 95 | - `fn` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Event handler, invoked with the target circle as first argument on 96 | _'centerchanged'_ and _'radiuschanged'_, or a _MapMouseEvent_ on _'click'_ and _'contextmenu'_ events 97 | - `onlyOnce` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** Remove handler after first call (optional, default `false`) 98 | 99 | Returns **[MapboxCircle](#mapboxcircle)** 100 | 101 | #### once 102 | 103 | Alias for registering event listener with _onlyOnce=true_, see [#on](#on). 104 | 105 | **Parameters** 106 | 107 | - `event` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Event name 108 | - `fn` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Event handler 109 | 110 | Returns **[MapboxCircle](#mapboxcircle)** 111 | 112 | #### off 113 | 114 | Unsubscribe to circle event. 115 | 116 | **Parameters** 117 | 118 | - `event` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Event name 119 | - `fn` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler to be removed 120 | 121 | Returns **[MapboxCircle](#mapboxcircle)** 122 | 123 | #### addTo 124 | 125 | **Parameters** 126 | 127 | - `map` **mapboxgl.Map** Target map for adding and initializing circle Mapbox GL layers/data/listeners. 128 | - `before` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Layer ID to insert the circle layers before; explicitly pass `null` to 129 | get the circle assets appended at the end of map-layers array (optional, default `'waterway-label'`) 130 | 131 | Returns **[MapboxCircle](#mapboxcircle)** 132 | 133 | #### remove 134 | 135 | Remove source data, layers and listeners from map. 136 | 137 | Returns **[MapboxCircle](#mapboxcircle)** 138 | 139 | #### getCenter 140 | 141 | Returns **{lat: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), lng: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)}** Circle center position 142 | 143 | #### setCenter 144 | 145 | **Parameters** 146 | 147 | - `position` **{lat: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), lng: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)}** 148 | 149 | Returns **[MapboxCircle](#mapboxcircle)** 150 | 151 | #### getRadius 152 | 153 | Returns **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Current radius, in meters 154 | 155 | #### setRadius 156 | 157 | **Parameters** 158 | 159 | - `newRadius` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Meter radius 160 | 161 | Returns **[MapboxCircle](#mapboxcircle)** 162 | 163 | #### getBounds 164 | 165 | Returns **{sw: {lat: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), lng: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)}, ne: {lat: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), lng: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)}}** Southwestern/northeastern bounds 166 | 167 | ## Development 168 | 169 | ### Install Dependencies 170 | 171 | npm install 172 | 173 | ### Run Locally 174 | 175 | npm start 176 | 177 | ### Build Development Bundle 178 | 179 | npm run browserify 180 | 181 | ### Build Distributable Package 182 | 183 | npm pack 184 | 185 | ### Update README API Documentation 186 | 187 | npm run docs 188 | 189 | ## Changelog 190 | 191 | ### v. 1.6.7 192 | 193 | - `optionalDependencies` removed from `package.json`, making this package easier to depend on ([#82](https://github.com/smithmicro/mapbox-gl-circle/issues/82)) 194 | - Bug fix for overlapping mouse-down events, causing the edit handle to lock until the user performs a full page refresh ([#80](https://github.com/smithmicro/mapbox-gl-circle/issues/80)) 195 | 196 | ### v. 1.6.6 197 | 198 | - New CI/CD integration, replacing Jenkins with GitHub Actions ([#93](https://github.com/smithmicro/mapbox-gl-circle/issues/93)) 199 | 200 | ### v. 1.6.5 201 | 202 | - Bug fix for layer switching in `mapbox-gl>0.40.1` ([#73](https://github.com/smithmicro/mapbox-gl-circle/issues/73)) 203 | - Half-fixed bug causing errors when adding circle to map style without the `waterway-label` layer 204 | 205 | ### v. 1.6.4 206 | 207 | - Performance improvements for Firefox and Edge on slow computers 208 | ([#64](https://github.com/smithmicro/mapbox-gl-circle/issues/64), 209 | [#59](https://github.com/smithmicro/mapbox-gl-circle/issues/59)) 210 | - Deprecated Docker build step 211 | 212 | ### v. 1.6.3 213 | 214 | - Transferring core project into SmithMicro organization, `mblomdahl/mapbox-gl-circle -> smithmicro/mapbox-gl-circle` 215 | 216 | ### v. 1.6.2 217 | 218 | - Handle center/radius drag interactions over Mapbox GL markers 219 | - Watch for removal of map container and handle removal 220 | 221 | ### v. 1.6.1 222 | 223 | - Improved move animation ([#55](https://github.com/smithmicro/mapbox-gl-circle/issues/55)) 224 | 225 | ### v. 1.6.0 226 | 227 | - Add optional `before` argument to _MapboxCircle.addTo_ 228 | ([#50](https://github.com/smithmicro/mapbox-gl-circle/issues/50)) 229 | - Updated center/radius handle interactions to make performance issues more subtle 230 | 231 | ### v. 1.5.2 232 | 233 | - Fix bug where the circle would always show a horizontal resize cursor on radius handles, 234 | irrespective of position (top/bottom/right/left) 235 | 236 | ### v. 1.5.1 237 | 238 | - Bug fixes with respect to cursor style when hovering over editable-and-clickable circles 239 | [SPFAM-1293](https://projects.smithmicro.net/browse/SPFAM-1293) 240 | 241 | ### v. 1.5.0 242 | 243 | - Added support for passing `minRadius` and `maxRadius` options to _MapboxCircle_ constructor 244 | 245 | ### v. 1.4.3 246 | 247 | - Bug fix for handling _map.setStyle_ updates 248 | - Added package version property to circle class 249 | 250 | ### v. 1.4.2 251 | 252 | - README updated with [Getting Started](#getting-started) section 253 | - Improved usage examples 254 | - Bug fixes: 255 | - Creating circle instances with bundler import failed 256 | - Docker build serving the wrong `index.html` 257 | 258 | ### v. 1.4.1 259 | 260 | - Performance and stability fixes 261 | 262 | ### v. 1.4.0 263 | 264 | - _MapboxCircle_ now supports subscribing to `click` and `contextmenu` (right-click) events 265 | 266 | ### v. 1.3.0 267 | 268 | - Added setters and getters for center/radius 269 | - _MapboxCircle_ now allows subscribing to events and fires `centerchanged`/`radiuschanged` on user modification 270 | - Improved API documentation + moved it into README / Usage 271 | 272 | ### v. 1.2.5 273 | 274 | - More bug fixes: 275 | - The circle can now successfully remove itself from the map 276 | - Multiple circles may be added to the map and edited without causing too much conflict 277 | - Initial center/radius drag interaction no longer fails 278 | 279 | ### v. 1.2.4 280 | 281 | - Bug fixes; passing `editable: false` when creating a circle is now respected, along with any styling options 282 | 283 | ### v. 1.2.3 284 | 285 | - Publishing releases as `@latest` and pre-releases as `@next` to 286 | 287 | - CI update for Docker image, now publishes releases and pre-releases to SMSI internal Docker registry, 288 | 289 | 290 | ### v. 1.2.2 291 | 292 | - CI updates, now integrates with GitHub and builds reliably (with unique version names) under 293 | 294 | 295 | ### v. 1.2.1 296 | 297 | - Added first-draft Jenkinsfile and started including `package-lock.json` 298 | - Revised `package.json` scripts 299 | 300 | ### v. 1.2.0 301 | 302 | - Removed dead code and unused methods 303 | - Restructured library, moving `circle.js -> lib/main.js` and `index.js -> example/index.js` 304 | - Refactored helper functions from `example/index.js` into _MapboxCircle_ class, obsoleted _index.html_ with 305 | DOM updates in _example/index.js_ 306 | - Refactor into _MapboxCircle_ into new-style ES6 class 307 | - Made _MapboxCircle.animate()_ and a bunch of properties private, added overridable defaults for 308 | fillColor/fillOpacity 309 | - Updated ESLint config to respect browser/commonjs built-ins and added docs to _MapboxCircle_ in order to 310 | align with ESLint JSDoc requirements 311 | - Updated project details in package.json and committed first-draft API documentation 312 | 313 | ### v. 1.1.0 314 | 315 | Updated circle from Mapbox [bl.ocks.org sample](https://bl.ocks.org/ryanbaumann/d286190943d6b4eb70e65a9f76eab5a5/d3cd7cea5feed0dfddbf3705b7936ff560f668d1). 316 | 317 | Now provides handles for modifying position/radius. Seems to also do better performance wise. 318 | 319 | ### v. 1.0.0 320 | 321 | The initial 1.0.0 release is a modified version of 322 | the [Draw-Circle.zip](https://www.dropbox.com/s/ya7am28y8eugd72/Draw-Circle.zip?dl=0) archive we got from Mapbox. 323 | 324 | Live demo of the original can be found here: 325 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const mapboxgl = require('mapbox-gl'); 5 | const MapboxCircle = require('../lib/main.js'); 6 | 7 | // eslint-disable-next-line 8 | console.log("Loaded MapboxCircle from 'mapbox-gl-circle-" + MapboxCircle.VERSION + "'"); 9 | 10 | const mapDiv = document.body.appendChild(document.createElement('div')); 11 | mapDiv.style.position = 'absolute'; 12 | mapDiv.style.top = '32px'; 13 | mapDiv.style.right = 0; 14 | mapDiv.style.left = 0; 15 | mapDiv.style.bottom = 0; 16 | 17 | const defaultStyle = 'streets'; 18 | 19 | const styleMenuDiv = document.body.appendChild(document.createElement('div')); 20 | styleMenuDiv.id = 'menu'; 21 | styleMenuDiv.style.position = 'absolute'; 22 | styleMenuDiv.style.left = 0; 23 | styleMenuDiv.style.bottom = 0; 24 | styleMenuDiv.style.backgroundColor = 'rgba(255, 255, 255, 0.5)'; 25 | 26 | for (let styleOption of ['basic', 'streets', 'bright', 'light', 'dark', 'satellite', 'satellite-streets']) { 27 | let inputEl = styleMenuDiv.appendChild(document.createElement('input')); 28 | inputEl.type = 'radio'; 29 | inputEl.name = 'styleSwitcher'; 30 | inputEl.id = inputEl.value = styleOption; 31 | if (styleOption === defaultStyle) { 32 | inputEl.checked = true; 33 | } 34 | 35 | inputEl.onclick = function setStyle(clickEvent) { 36 | map.setStyle('mapbox://styles/mapbox/' + clickEvent.target.id + '-v9'); 37 | }; 38 | 39 | let labelEl = styleMenuDiv.appendChild(document.createElement('label')); 40 | labelEl.for = labelEl.textContent = styleOption; 41 | labelEl.style.paddingRight = '10px'; 42 | } 43 | 44 | // noinspection SpellCheckingInspection 45 | mapboxgl.accessToken = 'pk.eyJ1IjoibWJsb21kYWhsLXNtc2kiLCJhIjoiY2w5ODVoYmtsMDk5NDN5cnE1bzN5Ym1nYiJ9.-mVDprrCsLXGRt7qyqFkWg'; 46 | 47 | const center = {lat: 39.984, lng: -75.343}; 48 | const map = new mapboxgl.Map({ 49 | container: mapDiv, 50 | style: 'mapbox://styles/mapbox/' + defaultStyle + '-v9', 51 | center: [center.lng, center.lat], 52 | zoom: 14 53 | }); 54 | 55 | window.map = map; 56 | 57 | const markerElement = document.createElement('div'); 58 | markerElement.style.backgroundImage = 'url(https://placekitten.com/g/50/)'; 59 | markerElement.style.width = '50px'; 60 | markerElement.style.height = '50px'; 61 | markerElement.style.borderRadius = '50%'; 62 | window.marker1 = new mapboxgl.Marker(markerElement) 63 | .setLngLat([center.lng, center.lat]) 64 | .addTo(map); 65 | 66 | // MapboxCircle Setup 67 | 68 | const editableOpts = { 69 | editable: true, 70 | strokeColor: '#29AB87', 71 | strokeWeight: 1, 72 | strokeOpacity: 0.85, 73 | fillColor: '#29AB87', 74 | fillOpacity: 0.2, 75 | minRadius: 100, 76 | maxRadius: 500000, 77 | debugEl: document.body.appendChild(document.createElement('div')) 78 | }; 79 | 80 | const extraPrettyEditableOpts = _.extend({refineStroke: true}, editableOpts); 81 | 82 | const nonEditableOpts = { 83 | strokeWeight: 0, 84 | fillColor: '#000000', 85 | fillOpacity: 0.2 86 | }; 87 | 88 | window.editableCircle0 = new MapboxCircle({lat: 39.986, lng: -75.341}, 350, editableOpts).addTo(map); 89 | 90 | window.plainCircle0 = new MapboxCircle({lat: 39.982, lng: -75.345}, 250, nonEditableOpts).addTo(map); 91 | 92 | window.plainCircle1 = new MapboxCircle({lat: 39.983, lng: -75.344}, 300, nonEditableOpts).addTo(map); 93 | 94 | window.editableCircle1 = new MapboxCircle({lat: 39.984, lng: -75.349}, 300, editableOpts).addTo(map) 95 | .setCenter({lat: 39.989, lng: -75.348}).setRadius(50); 96 | 97 | window.editableCircle2 = new MapboxCircle({lat: 39.974377, lng: -75.639449}, 25000, extraPrettyEditableOpts).addTo(map); 98 | window.editableCircle3 = new MapboxCircle({lat: 39.980, lng: -75.340}, 225, editableOpts).addTo(map); 99 | 100 | window.plainCircle2 = new MapboxCircle({lat: 39.983, lng: -75.345}, 150, nonEditableOpts).addTo(map); 101 | window.plainCircle3 = new MapboxCircle([-75.352, 39.983], 200, nonEditableOpts).addTo(map); 102 | 103 | window.setTimeout(function () { 104 | window.editableCircle1.remove(); 105 | window.editableCircle3.remove(); 106 | window.setTimeout(function () { 107 | window.editableCircle1.addTo(map).setCenter({lat: 39.984, lng: -75.349}).setRadius(300); 108 | window.editableCircle3.addTo(map); 109 | }, 1250); 110 | }, 2500); 111 | 112 | 113 | window.editableCircle2 114 | .on('radiuschanged', function (circleObj) { 115 | const newRadius = circleObj.getRadius(); 116 | // eslint-disable-next-line 117 | console.log('editableCircle2/radiuschanged', circleObj.getBounds()); 118 | window.setTimeout(function () { 119 | if (newRadius === circleObj.getRadius()) { 120 | circleObj.setRadius(newRadius * .99); 121 | } 122 | }, 1500); 123 | }) 124 | .on('centerchanged', function (circleObj) { 125 | // eslint-disable-next-line 126 | console.log('editableCircle2/centerchanged', circleObj.getCenter()); 127 | }) 128 | .on('radiuschanged', function (circleObj) { 129 | // eslint-disable-next-line 130 | console.log('editableCircle2/radiuschanged', circleObj.getRadius()); 131 | }) 132 | .on('click', function (mouseEvent) { 133 | // eslint-disable-next-line 134 | console.log('editableCircle2/click', mouseEvent); 135 | }) 136 | .on('contextmenu', function (mouseEvent) { 137 | // eslint-disable-next-line 138 | console.log('editableCircle2/contextmenu', mouseEvent); 139 | }) 140 | .on('click', function (mouseEvent) { 141 | // eslint-disable-next-line 142 | console.log('editableCircle2/click', mouseEvent); 143 | }) 144 | .on('contextmenu', function (mouseEvent) { 145 | // eslint-disable-next-line 146 | console.log('editableCircle2/contextmenu', mouseEvent); 147 | }); 148 | 149 | window.editableCircle3 150 | .on('radiuschanged', function (circleObj) { 151 | const newRadius = circleObj.getRadius(); 152 | // eslint-disable-next-line 153 | console.log('editableCircle3/radiuschanged', circleObj.getBounds()); 154 | window.setTimeout(function () { 155 | if (newRadius === circleObj.getRadius()) { 156 | circleObj.setRadius(newRadius * 1.01); 157 | } 158 | }, 1750); 159 | }) 160 | .on('centerchanged', function (circleObj) { 161 | // eslint-disable-next-line 162 | console.log('editableCircle3/centerchanged', circleObj.getCenter()); 163 | }) 164 | .on('click', function (mouseEvent) { 165 | // eslint-disable-next-line 166 | console.log('editableCircle3/click', mouseEvent); 167 | }) 168 | .on('contextmenu', function (mouseEvent) { 169 | // eslint-disable-next-line 170 | console.log('editableCircle3/contextmenu', mouseEvent); 171 | }); 172 | 173 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MapboxCircle Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 60 | 61 | 62 |
63 |
64 | Tips! Right-click on map to add new circle. Left-click on a circle to remove it. 65 |
66 |
67 |
68 | 85 |
86 |
87 | 88 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const projectVersion = require('../package.json').version; 4 | const _ = require('lodash'); 5 | const EventEmitter = require('events'); 6 | const turfCircle = require('@turf/circle'); 7 | const turfBbox = require('@turf/bbox'); 8 | const turfBboxPoly = require('@turf/bbox-polygon'); 9 | const turfTruncate = require('@turf/truncate'); 10 | const turfDestination = require('@turf/destination'); 11 | const turfDistance = require('@turf/distance'); 12 | const turfBearing = require('@turf/bearing'); 13 | const turfHelpers = require('@turf/helpers'); 14 | 15 | if (window && typeof window.MapboxCircle === 'function') { 16 | throw new TypeError('mapbox-gl-circle-' + window.MapboxCircle.VERSION + ' already loaded'); 17 | } 18 | 19 | /** 20 | * A `google.maps.Circle` replacement for Mapbox GL JS, rendering a "spherical cap" on top of the world. 21 | * @class MapboxCircle 22 | * @example 23 | * var myCircle = new MapboxCircle({lat: 39.984, lng: -75.343}, 25000, { 24 | * editable: true, 25 | * minRadius: 1500, 26 | * fillColor: '#29AB87' 27 | * }).addTo(myMapboxGlMap); 28 | * 29 | * myCircle.on('centerchanged', function (circleObj) { 30 | * console.log('New center:', circleObj.getCenter()); 31 | * }); 32 | * myCircle.once('radiuschanged', function (circleObj) { 33 | * console.log('New radius (once!):', circleObj.getRadius()); 34 | * }); 35 | * myCircle.on('click', function (mapMouseEvent) { 36 | * console.log('Click:', mapMouseEvent.point); 37 | * }); 38 | * myCircle.on('contextmenu', function (mapMouseEvent) { 39 | * console.log('Right-click:', mapMouseEvent.lngLat); 40 | * }); 41 | * @public 42 | */ 43 | class MapboxCircle { 44 | /** 45 | * @return {string} 'mapbox-gl-circle' library version number. 46 | */ 47 | static get VERSION() { 48 | return projectVersion; 49 | } 50 | 51 | /** 52 | * @return {number} Globally unique instance ID. 53 | * @private 54 | */ 55 | get _instanceId() { 56 | if (this.__instanceId === undefined) { 57 | this.__instanceId = MapboxCircle.__MONOSTATE.instanceIdCounter++; 58 | } 59 | return this.__instanceId; 60 | } 61 | 62 | /** 63 | * @return {string} Unique circle source ID. 64 | * @private 65 | */ 66 | get _circleSourceId() { 67 | return 'circle-source-' + this._instanceId; 68 | } 69 | 70 | /** 71 | * @return {string} Unique circle center handle source ID. 72 | * @private 73 | */ 74 | get _circleCenterHandleSourceId() { 75 | return 'circle-center-handle-source-' + this._instanceId; 76 | } 77 | 78 | /** 79 | * @return {string} Unique radius handles source ID. 80 | * @private 81 | */ 82 | get _circleRadiusHandlesSourceId() { 83 | return 'circle-radius-handles-source-' + this._instanceId; 84 | } 85 | 86 | /** 87 | * @return {string} Unique circle line-stroke ID. 88 | * @private 89 | */ 90 | get _circleStrokeId() { 91 | return 'circle-stroke-' + this._instanceId; 92 | } 93 | 94 | /** 95 | * @return {string} Unique circle fill ID. 96 | * @private 97 | */ 98 | get _circleFillId() { 99 | return 'circle-fill-' + this._instanceId; 100 | } 101 | 102 | /** 103 | * @return {string} Unique ID for center handle stroke. 104 | * @private 105 | */ 106 | get _circleCenterHandleStrokeId() { 107 | return 'circle-center-handle-stroke-' + this._instanceId; 108 | } 109 | 110 | /** 111 | * @return {string} Unique ID for radius handles stroke. 112 | * @private 113 | */ 114 | get _circleRadiusHandlesStrokeId() { 115 | return 'circle-radius-handles-stroke-' + this._instanceId; 116 | } 117 | 118 | /** 119 | * @return {string} Unique circle center handle ID. 120 | * @private 121 | */ 122 | get _circleCenterHandleId() { 123 | return 'circle-center-handle-' + this._instanceId; 124 | } 125 | 126 | /** 127 | * @return {string} Unique circle radius handles' ID. 128 | * @private 129 | */ 130 | get _circleRadiusHandlesId() { 131 | return 'circle-radius-handles-' + this._instanceId; 132 | } 133 | 134 | /** @param {mapboxgl.Map} map Target map. */ 135 | set map(map) { 136 | if (!this._map || !map) { 137 | this._map = map; 138 | } else { 139 | throw new TypeError('MapboxCircle.map reassignment.'); 140 | } 141 | } 142 | 143 | /** @return {mapboxgl.Map} Mapbox map. */ 144 | get map() { 145 | return this._map; 146 | } 147 | 148 | /** @param {[number,number]} newCenter Center `[lng, lat]` coordinates. */ 149 | set center(newCenter) { 150 | if (this._centerDragActive) { 151 | this._editCenterLngLat[0] = newCenter[0]; 152 | this._editCenterLngLat[1] = newCenter[1]; 153 | } else { 154 | this._currentCenterLngLat[0] = newCenter[0]; 155 | this._currentCenterLngLat[1] = newCenter[1]; 156 | } 157 | this._updateCircle(); 158 | this._animate(); 159 | } 160 | 161 | /** @return {[number,number]} Current center `[lng, lat]` coordinates. */ 162 | get center() { 163 | return this._centerDragActive ? this._editCenterLngLat : this._currentCenterLngLat; 164 | } 165 | 166 | /** @param {number} newRadius Meter radius. */ 167 | set radius(newRadius) { 168 | if (this._radiusDragActive) { 169 | this._editRadius = Math.min(Math.max(this.options.minRadius, newRadius), this.options.maxRadius); 170 | } else { 171 | this._currentRadius = Math.min(Math.max(this.options.minRadius, newRadius), this.options.maxRadius); 172 | } 173 | this._updateCircle(); 174 | this._animate(); 175 | } 176 | 177 | /** @return {number} Current circle radius. */ 178 | get radius() { 179 | return this._radiusDragActive ? this._editRadius : this._currentRadius; 180 | } 181 | 182 | /** @param {number} newZoom New zoom level. */ 183 | set zoom(newZoom) { 184 | this._zoom = newZoom; 185 | if (this.options.refineStroke) { 186 | this._updateCircle(); 187 | this._animate(); 188 | } 189 | } 190 | 191 | /** 192 | * @param {{lat: number, lng: number}|[number,number]} center Circle center as an object or `[lng, lat]` coordinates 193 | * @param {number} radius Meter radius 194 | * @param {?Object} options 195 | * @param {?boolean} [options.editable=false] Enable handles for changing center and radius 196 | * @param {?number} [options.minRadius=10] Minimum radius on user interaction 197 | * @param {?number} [options.maxRadius=1100000] Maximum radius on user interaction 198 | * @param {?string} [options.strokeColor='#000000'] Stroke color 199 | * @param {?number} [options.strokeWeight=0.5] Stroke weight 200 | * @param {?number} [options.strokeOpacity=0.75] Stroke opacity 201 | * @param {?string} [options.fillColor='#FB6A4A'] Fill color 202 | * @param {?number} [options.fillOpacity=0.25] Fill opacity 203 | * @param {?boolean} [options.refineStroke=false] Adjust circle polygon precision based on radius and zoom 204 | * (i.e. prettier circles at the expense of performance) 205 | * @param {?Object} [options.properties={}] Property metadata for Mapbox GL JS circle object 206 | * @public 207 | */ 208 | constructor(center, radius, options) { 209 | /** @const {boolean} */ this.__safariContextMenuEventHackEnabled = false; 210 | 211 | /** @const {EventEmitter} */ this._eventEmitter = new EventEmitter(); 212 | 213 | let centerLat = typeof(center.lat) === 'number' ? center.lat : center[1]; 214 | let centerLng = typeof(center.lng) === 'number' ? center.lng : center[0]; 215 | 216 | /** @const {[number,number]} */ this._lastCenterLngLat = [centerLng, centerLat]; 217 | /** @const {[number,number]} */ this._editCenterLngLat = [centerLng, centerLat]; 218 | /** @const {[number,number]} */ this._currentCenterLngLat = [centerLng, centerLat]; 219 | /** @const {number} */ this._lastRadius = Math.round(radius); 220 | /** @const {number} */ this._editRadius = Math.round(radius); 221 | /** @const {number} */ this._currentRadius = Math.round(radius); 222 | /** @const {Object} */ this.options = _.extend({ 223 | editable: false, 224 | strokeColor: '#000000', 225 | strokeWeight: 0.5, 226 | strokeOpacity: 0.75, 227 | fillColor: '#FB6A4A', 228 | fillOpacity: 0.25, 229 | refineStroke: false, 230 | minRadius: 10, 231 | maxRadius: 1.1e6, 232 | properties: {}, 233 | debugEl: null 234 | }, options); 235 | 236 | /** @const {mapboxgl.Map} */ this._map = undefined; 237 | /** @const {number} */ this._zoom = undefined; 238 | /** @const {Polygon} */ this._circle = undefined; 239 | /** @const {Array} */ this._handles = undefined; 240 | /** @const {boolean} */ this._centerDragActive = false; 241 | /** @const {boolean} */ this._radiusDragActive = false; 242 | /** @const {Object} */ this._debouncedHandlers = {}; 243 | /** @const {number} */ this._updateCount = 0; 244 | 245 | [ // Bind all event handlers. 246 | '_onZoomEnd', 247 | '_onCenterHandleMouseEnter', 248 | '_onCenterHandleResumeEvents', 249 | '_onCenterHandleSuspendEvents', 250 | '_onCenterHandleMouseDown', 251 | '_onCenterHandleMouseMove', 252 | '_onCenterHandleMouseUpOrMapMouseOut', 253 | '_onCenterChanged', 254 | '_onCenterHandleMouseLeave', 255 | '_onRadiusHandlesMouseEnter', 256 | '_onRadiusHandlesSuspendEvents', 257 | '_onRadiusHandlesResumeEvents', 258 | '_onRadiusHandlesMouseDown', 259 | '_onRadiusHandlesMouseMove', 260 | '_onRadiusHandlesMouseUpOrMapMouseOut', 261 | '_onRadiusChanged', 262 | '_onRadiusHandlesMouseLeave', 263 | '_onCircleFillMouseMove', 264 | '_onCircleFillSuspendEvents', 265 | '_onCircleFillResumeEvents', 266 | '_onCircleFillContextMenu', 267 | '_onCircleFillClick', 268 | '_onCircleFillMouseLeave', 269 | '_onMapStyleDataLoading' 270 | ].forEach((eventHandler) => { 271 | this[eventHandler] = this[eventHandler].bind(this); 272 | }); 273 | 274 | // Initialize circle. 275 | this._updateCircle(); 276 | } 277 | 278 | /** 279 | * Return `true` if current browser seems to be Safari. 280 | * @return {boolean} 281 | * @private 282 | */ 283 | static _checkIfBrowserIsSafari() { 284 | return window.navigator.userAgent.indexOf('Chrome') === -1 && window.navigator.userAgent.indexOf('Safari') > -1; 285 | } 286 | 287 | /** 288 | * Add debounced event handler to map. 289 | * @param {string} event Mapbox GL event name 290 | * @param {Function} handler Event handler 291 | * @private 292 | */ 293 | _mapOnDebounced(event, handler) { 294 | let ticking = false; 295 | this._debouncedHandlers[handler] = (args) => { 296 | if (!ticking) { 297 | requestAnimationFrame(() => { 298 | handler(args); 299 | ticking = false; 300 | }); 301 | } 302 | ticking = true; 303 | }; 304 | this.map.on(event, this._debouncedHandlers[handler]); 305 | } 306 | 307 | /** 308 | * Remove debounced event handler from map. 309 | * @param {string} event Mapbox GL event name 310 | * @param {Function} handler Event handler 311 | * @private 312 | */ 313 | _mapOffDebounced(event, handler) { 314 | this.map.off(event, this._debouncedHandlers[handler]); 315 | } 316 | 317 | /** 318 | * Re-calculate/update circle polygon and handles. 319 | * @private 320 | */ 321 | _updateCircle() { 322 | const center = this.center; 323 | const radius = this.radius; 324 | const zoom = !this._zoom || this._zoom <= 0.1 ? 0.1 : this._zoom; 325 | const steps = this.options.refineStroke ? Math.max((Math.sqrt(Math.trunc(radius * 0.25)) * zoom ^ 2), 64) : 64; 326 | const unit = 'meters'; 327 | 328 | if (!(this._centerDragActive && radius < 10000)) { 329 | this._circle = turfCircle(center, radius, steps, unit, this.options.properties); 330 | } 331 | if (this.options.editable) { 332 | this._handles = [ 333 | turfDestination(center, radius, 0, unit), 334 | turfDestination(center, radius, 90, unit), 335 | turfDestination(center, radius, 180, unit), 336 | turfDestination(center, radius, -90, unit) 337 | ]; 338 | } 339 | 340 | if (this.options.debugEl) { 341 | this._updateCount += 1; 342 | this.options.debugEl.innerHTML = ('Center: ' + JSON.stringify(this.getCenter()) + ' / Radius: ' + radius + 343 | ' / Bounds: ' + JSON.stringify(this.getBounds()) + ' / Steps: ' + steps + 344 | ' / Zoom: ' + zoom.toFixed(2) + ' / ID: ' + this._instanceId + 345 | ' / #: ' + this._updateCount); 346 | } 347 | } 348 | 349 | /** 350 | * Return GeoJSON for circle and handles. 351 | * @private 352 | * @return {FeatureCollection} 353 | */ 354 | _getCircleGeoJSON() { 355 | return turfHelpers.featureCollection([this._circle]); 356 | } 357 | 358 | /** 359 | * Return GeoJSON for center handle and stroke. 360 | * @private 361 | * @return {FeatureCollection} 362 | */ 363 | _getCenterHandleGeoJSON() { 364 | if (this._centerDragActive && this.radius < 10000) { 365 | return turfHelpers.featureCollection([turfHelpers.point(this.center)]); 366 | } else { 367 | return turfHelpers.featureCollection([turfHelpers.point(this.center), this._circle]); 368 | } 369 | } 370 | 371 | /** 372 | * Return GeoJSON for radius handles and stroke. 373 | * @private 374 | * @return {FeatureCollection} 375 | */ 376 | _getRadiusHandlesGeoJSON() { 377 | return turfHelpers.featureCollection([...this._handles, this._circle]); 378 | } 379 | 380 | /** 381 | * Refresh map with GeoJSON for circle/handles. 382 | * @private 383 | */ 384 | _animate() { 385 | if (!this._centerDragActive && !this._radiusDragActive) { 386 | this._map.getSource(this._circleSourceId).setData(this._getCircleGeoJSON()); 387 | } 388 | 389 | if (this.options.editable) { 390 | if (!this._radiusDragActive) { 391 | this._map.getSource(this._circleCenterHandleSourceId).setData(this._getCenterHandleGeoJSON()); 392 | } 393 | if (!this._centerDragActive) { 394 | this._map.getSource(this._circleRadiusHandlesSourceId).setData(this._getRadiusHandlesGeoJSON()); 395 | } 396 | } 397 | } 398 | 399 | /** 400 | * Returns true if cursor point is on a center/radius edit handle. 401 | * @param {{x: number, y: number}} point 402 | * @return {boolean} 403 | * @private 404 | */ 405 | _pointOnHandle(point) { 406 | return !MapboxCircle.__MONOSTATE.activeEditableCircles.every((circleWithHandles) => { 407 | // noinspection JSCheckFunctionSignatures 408 | const handleLayersAtCursor = this.map.queryRenderedFeatures( 409 | point, {layers: [circleWithHandles._circleCenterHandleId, circleWithHandles._circleRadiusHandlesId]}); 410 | return handleLayersAtCursor.length === 0; 411 | }); 412 | } 413 | 414 | /** 415 | * Broadcast suspend event to other interactive circles, instructing them to stop listening during drag interaction. 416 | * @param {string} typeOfHandle 'radius' or 'circle'. 417 | * @private 418 | */ 419 | _suspendHandleListeners(typeOfHandle) { 420 | MapboxCircle.__MONOSTATE.broadcast.emit('suspendCenterHandleListeners', this._instanceId, typeOfHandle); 421 | MapboxCircle.__MONOSTATE.broadcast.emit('suspendRadiusHandlesListeners', this._instanceId, typeOfHandle); 422 | MapboxCircle.__MONOSTATE.broadcast.emit('suspendCircleFillListeners', this._instanceId, typeOfHandle); 423 | } 424 | 425 | /** 426 | * Broadcast resume event to other editable circles, to make them to resume interactivity after a completed drag op. 427 | * @param {string} typeOfHandle 'radius' or 'circle'. 428 | * @private 429 | */ 430 | _resumeHandleListeners(typeOfHandle) { 431 | MapboxCircle.__MONOSTATE.broadcast.emit('resumeCenterHandleListeners', this._instanceId, typeOfHandle); 432 | MapboxCircle.__MONOSTATE.broadcast.emit('resumeRadiusHandlesListeners', this._instanceId, typeOfHandle); 433 | MapboxCircle.__MONOSTATE.broadcast.emit('resumeCircleFillListeners', this._instanceId, typeOfHandle); 434 | } 435 | 436 | /** 437 | * Disable map panning, set cursor style and highlight handle with new fill color. 438 | * @param {string} layerId 439 | * @param {string} cursor 440 | * @private 441 | */ 442 | _highlightHandles(layerId, cursor) { 443 | this.map.dragPan.disable(); 444 | this.map.setPaintProperty(layerId, 'circle-color', this.options.fillColor); 445 | this.map.getCanvas().style.cursor = cursor; 446 | } 447 | 448 | /** 449 | * Re-enable map panning, reset cursor icon and restore fill color to white. 450 | * @param {string} layerId 451 | * @private 452 | */ 453 | _resetHandles(layerId) { 454 | this.map.dragPan.enable(); 455 | this.map.setPaintProperty(layerId, 'circle-color', '#ffffff'); 456 | this.map.getCanvas().style.cursor = ''; 457 | } 458 | 459 | /** 460 | * Adjust circle precision (steps used to draw the polygon). 461 | * @private 462 | */ 463 | _onZoomEnd() { 464 | this.zoom = this.map.getZoom(); 465 | } 466 | 467 | /** 468 | * Highlight center handle and disable panning. 469 | * @private 470 | */ 471 | _onCenterHandleMouseEnter() { 472 | this._highlightHandles(this._circleCenterHandleId, 'move'); 473 | } 474 | 475 | /** 476 | * Stop listening to center handle events, unless it's what the circle is currently busy with. 477 | * @param {number} instanceId ID of the circle instance that requested suspension. 478 | * @param {string} typeOfHandle 'center' or 'radius'. 479 | * @private 480 | */ 481 | _onCenterHandleSuspendEvents(instanceId, typeOfHandle) { 482 | if (instanceId !== this._instanceId || typeOfHandle === 'radius') { 483 | this._unbindCenterHandleListeners(); 484 | } 485 | } 486 | 487 | /** 488 | * Start listening to center handle events again, unless the circle was NOT among those targeted by suspend event. 489 | * @param {number} instanceId ID of the circle instance that said it's time to resume listening. 490 | * @param {string} typeOfHandle 'center' or 'radius'. 491 | * @private 492 | */ 493 | _onCenterHandleResumeEvents(instanceId, typeOfHandle) { 494 | if (instanceId !== this._instanceId || typeOfHandle === 'radius') { 495 | this._bindCenterHandleListeners(); 496 | } 497 | } 498 | 499 | /** 500 | * Highlight center handle, disable panning and add mouse-move listener (emulating drag until mouse-up event). 501 | * @private 502 | */ 503 | _onCenterHandleMouseDown() { 504 | if (this._getCursorStyle() !== 'move') { 505 | /* Only trigger center edit event if the user expects it. */ return; 506 | } 507 | this._centerDragActive = true; 508 | this._mapOnDebounced('mousemove', this._onCenterHandleMouseMove); 509 | this.map.addLayer(this._getCenterHandleStrokeLayer(), this._circleCenterHandleId); 510 | this._suspendHandleListeners('center'); 511 | this.map.once('mouseup', this._onCenterHandleMouseUpOrMapMouseOut); 512 | this.map.once('mouseout', this._onCenterHandleMouseUpOrMapMouseOut); // Deactivate drag if mouse leaves canvas. 513 | this._highlightHandles(this._circleCenterHandleId, 'move'); 514 | } 515 | 516 | /** 517 | * Animate circle center change after _onCenterHandleMouseDown triggers. 518 | * @param {MapMouseEvent} event 519 | * @private 520 | */ 521 | _onCenterHandleMouseMove(event) { 522 | const mousePoint = turfTruncate(turfHelpers.point(this.map.unproject(event.point).toArray()), 6); 523 | this.center = mousePoint.geometry.coordinates; 524 | } 525 | 526 | /** 527 | * Reset center handle, re-enable panning and remove listeners from _onCenterHandleMouseDown. 528 | * @param {MapMouseEvent} event 529 | * @private 530 | */ 531 | _onCenterHandleMouseUpOrMapMouseOut(event) { 532 | if (event.type === 'mouseout') { 533 | const toMarker = event.originalEvent.toElement.classList.contains('mapboxgl-marker'); 534 | const fromCanvas = event.originalEvent.fromElement.classList.contains('mapboxgl-canvas'); 535 | 536 | const toCanvas = event.originalEvent.toElement.classList.contains('mapboxgl-canvas'); 537 | const fromMarker = event.originalEvent.fromElement.classList.contains('mapboxgl-marker'); 538 | 539 | if ((fromCanvas && toMarker) || (fromMarker && toCanvas)) { 540 | this.map.once('mouseout', this._onCenterHandleMouseUpOrMapMouseOut); // Add back 'once' handler. 541 | return; 542 | } 543 | } 544 | 545 | const newCenter = this.center; 546 | this._centerDragActive = false; 547 | this._mapOffDebounced('mousemove', this._onCenterHandleMouseMove); 548 | switch (event.type) { 549 | case 'mouseup': this.map.off('mouseout', this._onCenterHandleMouseUpOrMapMouseOut); break; 550 | case 'mouseout': this.map.off('mouseup', this._onCenterHandleMouseUpOrMapMouseOut); break; 551 | } 552 | this._resumeHandleListeners('center'); 553 | this.map.removeLayer(this._circleCenterHandleStrokeId); 554 | this._resetHandles(this._circleCenterHandleId); 555 | if (newCenter[0] !== this._lastCenterLngLat[0] || newCenter[1] !== this._lastCenterLngLat[1]) { 556 | this.center = newCenter; 557 | this._eventEmitter.emit('centerchanged', this); 558 | } 559 | } 560 | 561 | /** 562 | * Update _lastCenterLngLat on `centerchanged` event. 563 | * @private 564 | */ 565 | _onCenterChanged() { 566 | this._lastCenterLngLat[0] = this.center[0]; 567 | this._lastCenterLngLat[1] = this.center[1]; 568 | } 569 | 570 | /** 571 | * Reset center handle and re-enable panning, unless actively dragging. 572 | * @private 573 | */ 574 | _onCenterHandleMouseLeave() { 575 | if (this._centerDragActive) { 576 | setTimeout(() => { // If dragging, wait a bit to see if it just recently stopped. 577 | if (!this._centerDragActive) this._resetHandles(this._circleCenterHandleId); 578 | }, 125); 579 | } else { 580 | this._resetHandles(this._circleCenterHandleId); 581 | } 582 | } 583 | 584 | /** 585 | * Return vertical or horizontal resize arrow depending on if mouse is at left-right or top-bottom edit handles. 586 | * @param {MapMouseEvent} event 587 | * @return {string} 'ew-resize' or 'ns-resize' 588 | * @private 589 | */ 590 | _getRadiusHandleCursorStyle(event) { 591 | const bearing = turfBearing(event.lngLat.toArray(), this._currentCenterLngLat, true); 592 | 593 | if (bearing > 270+45 || bearing <= 45) { // South. 594 | return 'ns-resize'; 595 | } 596 | if (bearing > 45 && bearing <= 90+45) { // West. 597 | return 'ew-resize'; 598 | } 599 | if (bearing > 90+45 && bearing <= 180+45) { // North. 600 | return 'ns-resize'; 601 | } 602 | if (bearing > 270-45 && bearing <= 270+45) { // East. 603 | return 'ew-resize'; 604 | } 605 | } 606 | 607 | /** 608 | * Highlight radius handles and disable panning. 609 | * @param {MapMouseEvent} event 610 | * @private 611 | */ 612 | _onRadiusHandlesMouseEnter(event) { 613 | this._highlightHandles(this._circleRadiusHandlesId, this._getRadiusHandleCursorStyle(event)); 614 | } 615 | 616 | /** 617 | * Stop listening to radius handles' events, unless it's what the circle is currently busy with. 618 | * @param {number} instanceId ID of the circle instance that requested suspension. 619 | * @param {string} typeOfHandle 'center' or 'radius'. 620 | * @private 621 | */ 622 | _onRadiusHandlesSuspendEvents(instanceId, typeOfHandle) { 623 | if (instanceId !== this._instanceId || typeOfHandle === 'center') { 624 | this._unbindRadiusHandlesListeners(); 625 | } 626 | } 627 | 628 | /** 629 | * Start listening to radius handles' events again, unless the circle was NOT among those targeted by suspend event. 630 | * @param {number} instanceId ID of the circle instance that said it's time to resume listening. 631 | * @param {string} typeOfHandle 'center' or 'radius'. 632 | * @private 633 | */ 634 | _onRadiusHandlesResumeEvents(instanceId, typeOfHandle) { 635 | if (instanceId !== this._instanceId || typeOfHandle === 'center') { 636 | this._bindRadiusHandlesListeners(); 637 | } 638 | } 639 | 640 | /** 641 | * Highlight radius handles, disable panning and add mouse-move listener (emulating drag until mouse-up event). 642 | * @param {MapMouseEvent} event 643 | * @private 644 | */ 645 | _onRadiusHandlesMouseDown(event) { 646 | if (!this._getCursorStyle().endsWith('-resize')) { 647 | /* Only trigger radius edit event if the user expects it. */ return; 648 | } 649 | this._radiusDragActive = true; 650 | this._mapOnDebounced('mousemove', this._onRadiusHandlesMouseMove); 651 | this.map.addLayer(this._getRadiusHandlesStrokeLayer(), this._circleRadiusHandlesId); 652 | this._suspendHandleListeners('radius'); 653 | this.map.once('mouseup', this._onRadiusHandlesMouseUpOrMapMouseOut); 654 | this.map.once('mouseout', this._onRadiusHandlesMouseUpOrMapMouseOut); // Deactivate drag if mouse leaves canvas. 655 | this._highlightHandles(this._circleRadiusHandlesId, this._getRadiusHandleCursorStyle(event)); 656 | } 657 | 658 | /** 659 | * Animate circle radius change after _onRadiusHandlesMouseDown triggers. 660 | * @param {MapMouseEvent} event 661 | * @private 662 | */ 663 | _onRadiusHandlesMouseMove(event) { 664 | const mousePoint = this.map.unproject(event.point).toArray(); 665 | this.radius = Math.round(turfDistance(this.center, mousePoint, 'meters')); 666 | } 667 | 668 | /** 669 | * Reset radius handles, re-enable panning and remove listeners from _onRadiusHandlesMouseDown. 670 | * @param {MapMouseEvent} event 671 | * @private 672 | */ 673 | _onRadiusHandlesMouseUpOrMapMouseOut(event) { 674 | if (event.type === 'mouseout') { 675 | const toMarker = event.originalEvent.toElement.classList.contains('mapboxgl-marker'); 676 | const fromCanvas = event.originalEvent.fromElement.classList.contains('mapboxgl-canvas'); 677 | 678 | const toCanvas = event.originalEvent.toElement.classList.contains('mapboxgl-canvas'); 679 | const fromMarker = event.originalEvent.fromElement.classList.contains('mapboxgl-marker'); 680 | 681 | if ((fromCanvas && toMarker) || (fromMarker && toCanvas)) { 682 | this.map.once('mouseout', this._onRadiusHandlesMouseUpOrMapMouseOut); // Add back 'once' handler. 683 | return; 684 | } 685 | } 686 | 687 | const newRadius = this.radius; 688 | this._radiusDragActive = false; 689 | this._mapOffDebounced('mousemove', this._onRadiusHandlesMouseMove); 690 | this.map.removeLayer(this._circleRadiusHandlesStrokeId); 691 | switch (event.type) { 692 | case 'mouseup': this.map.off('mouseout', this._onRadiusHandlesMouseUpOrMapMouseOut); break; 693 | case 'mouseout': this.map.off('mouseup', this._onRadiusHandlesMouseUpOrMapMouseOut); break; 694 | } 695 | this._resumeHandleListeners('radius'); 696 | this._resetHandles(this._circleRadiusHandlesId); 697 | if (newRadius !== this._lastRadius) { 698 | this.radius = newRadius; 699 | this._eventEmitter.emit('radiuschanged', this); 700 | } 701 | } 702 | 703 | /** 704 | * Update _lastRadius on `radiuschanged` event. 705 | * @private 706 | */ 707 | _onRadiusChanged() { 708 | this._lastRadius = this.radius; 709 | } 710 | 711 | /** 712 | * Reset radius handles and re-enable panning, unless actively dragging. 713 | * @private 714 | */ 715 | _onRadiusHandlesMouseLeave() { 716 | if (this._radiusDragActive) { 717 | setTimeout(() => { // If dragging, wait a bit to see if it just recently stopped. 718 | if (!this._radiusDragActive) this._resetHandles(this._circleRadiusHandlesId); 719 | }, 125); 720 | } else { 721 | this._resetHandles(this._circleRadiusHandlesId); 722 | } 723 | } 724 | 725 | /** 726 | * Set pointer cursor when moving over circle fill, and it's clickable. 727 | * @param {MapMouseEvent} event 728 | * @private 729 | */ 730 | _onCircleFillMouseMove(event) { 731 | if (this._eventEmitter.listeners('click').length > 0 && !this._pointOnHandle(event.point)) { 732 | event.target.getCanvas().style.cursor = 'pointer'; 733 | } 734 | } 735 | 736 | /** 737 | * Stop listening to circle fill events. 738 | * @private 739 | */ 740 | _onCircleFillSuspendEvents() { 741 | this._unbindCircleFillListeners(); 742 | } 743 | 744 | /** 745 | * Start listening to circle fill events again. 746 | * @private 747 | */ 748 | _onCircleFillResumeEvents() { 749 | this._bindCircleFillListeners(); 750 | } 751 | 752 | /** 753 | * Fire 'contextmenu' event. 754 | * @param {MapMouseEvent} event 755 | * @private 756 | */ 757 | _onCircleFillContextMenu(event) { 758 | if (this._pointOnHandle(event.point)) { 759 | /* No click events while on a center/radius edit handle. */ return; 760 | } 761 | 762 | if (event.originalEvent.ctrlKey && MapboxCircle._checkIfBrowserIsSafari()) { 763 | // This hack comes from SPFAM-1090, aimed towards eliminating the extra 'click' event that's 764 | // emitted by Safari when performing a right-click by holding the ctrl button. 765 | this.__safariContextMenuEventHackEnabled = true; 766 | } else { 767 | this._eventEmitter.emit('contextmenu', event); 768 | } 769 | } 770 | 771 | /** 772 | * Fire 'click' event. 773 | * @param {MapMouseEvent} event 774 | * @private 775 | */ 776 | _onCircleFillClick(event) { 777 | if (this._pointOnHandle(event.point)) { 778 | /* No click events while on a center/radius edit handle. */ return; 779 | } 780 | 781 | if (!this.__safariContextMenuEventHackEnabled) { 782 | this._eventEmitter.emit('click', event); 783 | } else { 784 | this._eventEmitter.emit('contextmenu', event); 785 | this.__safariContextMenuEventHackEnabled = false; 786 | } 787 | } 788 | 789 | /** 790 | * Remove pointer cursor when leaving circle fill. 791 | * @param {MapMouseEvent} event 792 | * @private 793 | */ 794 | _onCircleFillMouseLeave(event) { 795 | if (this._eventEmitter.listeners('click').length > 0 && !this._pointOnHandle(event.point)) { 796 | event.target.getCanvas().style.cursor = ''; 797 | } 798 | } 799 | 800 | /** 801 | * When map style is changed, remove circle assets from map and add it back on next MapboxGL 'styledata' event. 802 | * @param {MapDataEvent} event 803 | * @private 804 | */ 805 | _onMapStyleDataLoading(event) { 806 | if (this.map) { 807 | this.map.once('styledata', () => { 808 | // noinspection JSUnresolvedVariable 809 | this.addTo(event.target); 810 | }); 811 | this.remove(); 812 | } 813 | } 814 | 815 | /** 816 | * Add all static listeners for center handle. 817 | * @param {mapboxgl.Map} [map] 818 | * @private 819 | */ 820 | _bindCenterHandleListeners(map) { 821 | map = map || this.map; 822 | const layerId = this._circleCenterHandleId; 823 | map.on('mouseenter', layerId, this._onCenterHandleMouseEnter); 824 | map.on('mousedown', layerId, this._onCenterHandleMouseDown); 825 | map.on('mouseleave', layerId, this._onCenterHandleMouseLeave); 826 | } 827 | 828 | /** 829 | * Remove all static listeners for center handle. 830 | * @param {mapboxgl.Map} [map] 831 | * @private 832 | */ 833 | _unbindCenterHandleListeners(map) { 834 | map = map || this.map; 835 | const layerId = this._circleCenterHandleId; 836 | map.off('mouseenter', layerId, this._onCenterHandleMouseEnter); 837 | map.off('mousedown', layerId, this._onCenterHandleMouseDown); 838 | map.off('mouseleave', layerId, this._onCenterHandleMouseLeave); 839 | } 840 | 841 | /** 842 | * Add all static listeners for radius handles. 843 | * @param {mapboxgl.Map} [map] 844 | * @private 845 | */ 846 | _bindRadiusHandlesListeners(map) { 847 | map = map || this.map; 848 | const layerId = this._circleRadiusHandlesId; 849 | map.on('mouseenter', layerId, this._onRadiusHandlesMouseEnter); 850 | map.on('mousedown', layerId, this._onRadiusHandlesMouseDown); 851 | map.on('mouseleave', layerId, this._onRadiusHandlesMouseLeave); 852 | } 853 | 854 | /** 855 | * Remove all static listeners for radius handles. 856 | * @param {mapboxgl.Map} [map] 857 | * @private 858 | */ 859 | _unbindRadiusHandlesListeners(map) { 860 | map = map || this.map; 861 | const layerId = this._circleRadiusHandlesId; 862 | map.off('mouseenter', layerId, this._onRadiusHandlesMouseEnter); 863 | map.off('mousedown', layerId, this._onRadiusHandlesMouseDown); 864 | map.off('mouseleave', layerId, this._onRadiusHandlesMouseLeave); 865 | } 866 | 867 | /** 868 | * Add all click/contextmenu listeners for circle fill layer. 869 | * @param {mapboxgl.Map} [map] 870 | * @private 871 | */ 872 | _bindCircleFillListeners(map) { 873 | map = map || this.map; 874 | const layerId = this._circleFillId; 875 | map.on('click', layerId, this._onCircleFillClick); 876 | map.on('contextmenu', layerId, this._onCircleFillContextMenu); 877 | map.on('mousemove', layerId, this._onCircleFillMouseMove); 878 | map.on('mouseleave', layerId, this._onCircleFillMouseLeave); 879 | } 880 | 881 | /** 882 | * Remove all click/contextmenu listeners for circle fill. 883 | * @param {mapboxgl.Map} [map] 884 | * @private 885 | */ 886 | _unbindCircleFillListeners(map) { 887 | map = map || this.map; 888 | const layerId = this._circleFillId; 889 | map.off('click', layerId, this._onCircleFillClick); 890 | map.off('contextmenu', layerId, this._onCircleFillContextMenu); 891 | map.off('mousemove', layerId, this._onCircleFillMouseMove); 892 | map.off('mouseleave', layerId, this._onCircleFillMouseLeave); 893 | } 894 | 895 | /** 896 | * Add suspend/resume listeners for `__MONOSTATE.broadcast` event emitter. 897 | * @private 898 | */ 899 | _bindBroadcastListeners() { 900 | MapboxCircle.__MONOSTATE.broadcast.on('suspendCenterHandleListeners', this._onCenterHandleSuspendEvents); 901 | MapboxCircle.__MONOSTATE.broadcast.on('resumeCenterHandleListeners', this._onCenterHandleResumeEvents); 902 | 903 | MapboxCircle.__MONOSTATE.broadcast.on('suspendRadiusHandlesListeners', this._onRadiusHandlesSuspendEvents); 904 | MapboxCircle.__MONOSTATE.broadcast.on('resumeRadiusHandlesListeners', this._onRadiusHandlesResumeEvents); 905 | 906 | MapboxCircle.__MONOSTATE.broadcast.on('suspendCircleFillListeners', this._onCircleFillSuspendEvents); 907 | MapboxCircle.__MONOSTATE.broadcast.on('resumeCircleFillListeners', this._onCircleFillResumeEvents); 908 | } 909 | 910 | /** 911 | * Remove suspend/resume handlers from `__MONOSTATE.broadcast` emitter. 912 | * @private 913 | */ 914 | _unbindBroadcastListeners() { 915 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 916 | 'suspendCenterHandleListeners', this._onCenterHandleSuspendEvents); 917 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 918 | 'resumeCenterHandleListeners', this._onCenterHandleResumeEvents); 919 | 920 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 921 | 'suspendRadiusHandlesListeners', this._onRadiusHandlesSuspendEvents); 922 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 923 | 'resumeRadiusHandlesListeners', this._onRadiusHandlesResumeEvents); 924 | 925 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 926 | 'suspendCircleFillListeners', this._onCircleFillSuspendEvents); 927 | MapboxCircle.__MONOSTATE.broadcast.removeListener( 928 | 'resumeCircleFillListeners', this._onCircleFillResumeEvents); 929 | } 930 | 931 | /** 932 | * Add circle to `__MONOSTATE.activeEditableCircles` array and increase max broadcasting listeners by 1. 933 | * @param {MapboxCircle} circleObject 934 | * @private 935 | */ 936 | static _addActiveEditableCircle(circleObject) { 937 | MapboxCircle.__MONOSTATE.activeEditableCircles.push(circleObject); 938 | MapboxCircle.__MONOSTATE.broadcast.setMaxListeners( 939 | MapboxCircle.__MONOSTATE.activeEditableCircles.length); 940 | } 941 | 942 | /** 943 | * Remove circle from `__MONOSTATE.activeEditableCircles` array and decrease max broadcasting listeners by 1. 944 | * @param {MapboxCircle} circleObject 945 | * @private 946 | */ 947 | static _removeActiveEditableCircle(circleObject) { 948 | MapboxCircle.__MONOSTATE.activeEditableCircles.splice( 949 | MapboxCircle.__MONOSTATE.activeEditableCircles.indexOf(circleObject), 1); 950 | MapboxCircle.__MONOSTATE.broadcast.setMaxListeners( 951 | MapboxCircle.__MONOSTATE.activeEditableCircles.length); 952 | } 953 | 954 | /** 955 | * @return {Object} GeoJSON map source for the circle. 956 | * @private 957 | */ 958 | _getCircleMapSource() { 959 | return { 960 | type: 'geojson', 961 | data: this._getCircleGeoJSON(), 962 | buffer: 1 963 | }; 964 | } 965 | 966 | /** 967 | * @return {Object} GeoJSON map source for center handle. 968 | * @private 969 | */ 970 | _getCenterHandleMapSource() { 971 | return { 972 | type: 'geojson', 973 | data: this._getCenterHandleGeoJSON(), 974 | buffer: 1 975 | }; 976 | } 977 | 978 | /** 979 | * @return {Object} GeoJSON map source for radius handles. 980 | * @private 981 | */ 982 | _getRadiusHandlesMapSource() { 983 | return { 984 | type: 'geojson', 985 | data: this._getRadiusHandlesGeoJSON(), 986 | buffer: 1 987 | }; 988 | } 989 | 990 | /** 991 | * @return {Object} Style layer for the stroke around the circle. 992 | * @private 993 | */ 994 | _getCircleStrokeLayer() { 995 | return { 996 | id: this._circleStrokeId, 997 | type: 'line', 998 | source: this._circleSourceId, 999 | paint: { 1000 | 'line-color': this.options.strokeColor, 1001 | 'line-width': this.options.strokeWeight, 1002 | 'line-opacity': this.options.strokeOpacity 1003 | }, 1004 | filter: ['==', '$type', 'Polygon'] 1005 | }; 1006 | } 1007 | 1008 | /** 1009 | * @return {Object} Style layer for the circle fill. 1010 | * @private 1011 | */ 1012 | _getCircleFillLayer() { 1013 | return { 1014 | id: this._circleFillId, 1015 | type: 'fill', 1016 | source: this._circleSourceId, 1017 | paint: { 1018 | 'fill-color': this.options.fillColor, 1019 | 'fill-opacity': this.options.fillOpacity 1020 | }, 1021 | filter: ['==', '$type', 'Polygon'] 1022 | }; 1023 | } 1024 | 1025 | /** 1026 | * @return {Object} Style layer for the center handle's stroke. 1027 | * @private 1028 | */ 1029 | _getCenterHandleStrokeLayer() { 1030 | if (this._centerDragActive && this.radius < 10000) { 1031 | // Inspired by node_modules/mapbox-gl/src/ui/control/scale_control.js:getDistance 1032 | const y = this.map._container.clientHeight / 2; 1033 | const x = this.map._container.clientWidth; 1034 | const horizontalPixelsPerMeter = x / turfDistance( 1035 | this.map.unproject([0, y]).toArray(), this.map.unproject([x, y]).toArray(), 'meters'); 1036 | return { 1037 | id: this._circleCenterHandleStrokeId, 1038 | type: 'circle', 1039 | source: this._circleCenterHandleSourceId, 1040 | paint: { 1041 | 'circle-radius': horizontalPixelsPerMeter * this.radius, 1042 | 'circle-opacity': 0, 1043 | 'circle-stroke-color': this.options.strokeColor, 1044 | 'circle-stroke-opacity': this.options.strokeOpacity * .5, 1045 | 'circle-stroke-width': this.options.strokeWeight 1046 | }, 1047 | filter: ['==', '$type', 'Point'] 1048 | }; 1049 | } else { 1050 | return { 1051 | id: this._circleCenterHandleStrokeId, 1052 | type: 'line', 1053 | source: this._circleCenterHandleSourceId, 1054 | paint: { 1055 | 'line-color': this.options.strokeColor, 1056 | 'line-width': this.options.strokeWeight, 1057 | 'line-opacity': this.options.strokeOpacity * 0.5 1058 | }, 1059 | filter: ['==', '$type', 'Polygon'] 1060 | }; 1061 | } 1062 | } 1063 | 1064 | /** 1065 | * @return {Object} Style layer for the radius handles' stroke. 1066 | * @private 1067 | */ 1068 | _getRadiusHandlesStrokeLayer() { 1069 | return { 1070 | id: this._circleRadiusHandlesStrokeId, 1071 | type: 'line', 1072 | source: this._circleRadiusHandlesSourceId, 1073 | paint: { 1074 | 'line-color': this.options.strokeColor, 1075 | 'line-width': this.options.strokeWeight, 1076 | 'line-opacity': this.options.strokeOpacity * 0.5 1077 | }, 1078 | filter: ['==', '$type', 'Polygon'] 1079 | }; 1080 | } 1081 | 1082 | /** 1083 | * @return {Object} Default paint style for edit handles. 1084 | * @private 1085 | */ 1086 | _getEditHandleDefaultPaintOptions() { 1087 | return { 1088 | 'circle-color': '#ffffff', 1089 | 'circle-radius': 3.75, 1090 | 'circle-stroke-color': this.options.strokeColor, 1091 | 'circle-stroke-opacity': this.options.strokeOpacity, 1092 | 'circle-stroke-width': this.options.strokeWeight 1093 | }; 1094 | } 1095 | 1096 | /** 1097 | * @return {Object} Style layer for the circle's center handle. 1098 | * @private 1099 | */ 1100 | _getCircleCenterHandleLayer() { 1101 | return { 1102 | id: this._circleCenterHandleId, 1103 | type: 'circle', 1104 | source: this._circleCenterHandleSourceId, 1105 | paint: this._getEditHandleDefaultPaintOptions(), 1106 | filter: ['==', '$type', 'Point'] 1107 | }; 1108 | } 1109 | 1110 | /** 1111 | * @return {Object} Style layer for the circle's radius handles. 1112 | * @private 1113 | */ 1114 | _getCircleRadiusHandlesLayer() { 1115 | return { 1116 | id: this._circleRadiusHandlesId, 1117 | type: 'circle', 1118 | source: this._circleRadiusHandlesSourceId, 1119 | paint: this._getEditHandleDefaultPaintOptions(), 1120 | filter: ['==', '$type', 'Point'] 1121 | }; 1122 | } 1123 | 1124 | /** 1125 | * @return {string} Current cursor style 1126 | * @private 1127 | */ 1128 | _getCursorStyle() { 1129 | return this.map.getCanvas().style.cursor; 1130 | } 1131 | 1132 | /** 1133 | * Subscribe to circle event. 1134 | * @param {string} event Event name; `click`, `contextmenu`, `centerchanged` or `radiuschanged` 1135 | * @param {Function} fn Event handler, invoked with the target circle as first argument on 1136 | * *'centerchanged'* and *'radiuschanged'*, or a *MapMouseEvent* on *'click'* and *'contextmenu'* events 1137 | * @param {?boolean} [onlyOnce=false] Remove handler after first call 1138 | * @return {MapboxCircle} 1139 | * @public 1140 | */ 1141 | on(event, fn, onlyOnce) { 1142 | if (onlyOnce) { 1143 | this._eventEmitter.once(event, fn); 1144 | } else { 1145 | this._eventEmitter.addListener(event, fn); 1146 | } 1147 | return this; 1148 | } 1149 | 1150 | /** 1151 | * Alias for registering event listener with *onlyOnce=true*, see {@link #on}. 1152 | * @param {string} event Event name 1153 | * @param {Function} fn Event handler 1154 | * @return {MapboxCircle} 1155 | * @public 1156 | */ 1157 | once(event, fn) { 1158 | return this.on(event, fn, true); 1159 | } 1160 | 1161 | /** 1162 | * Unsubscribe to circle event. 1163 | * @param {string} event Event name 1164 | * @param {Function} fn Handler to be removed 1165 | * @return {MapboxCircle} 1166 | * @public 1167 | */ 1168 | off(event, fn) { 1169 | this._eventEmitter.removeListener(event, fn); 1170 | return this; 1171 | } 1172 | 1173 | /** 1174 | * @param {mapboxgl.Map} map Target map for adding and initializing circle Mapbox GL layers/data/listeners. 1175 | * @param {?string} [before='waterway-label'] Layer ID to insert the circle layers before; explicitly pass `null` to 1176 | * get the circle assets appended at the end of map-layers array 1177 | * @return {MapboxCircle} 1178 | * @public 1179 | */ 1180 | addTo(map, before) { 1181 | if (typeof before === 'undefined' && map.getLayer('waterway-label')) { 1182 | before = 'waterway-label'; 1183 | } 1184 | const addCircleAssetsOnMap = () => { 1185 | map.addSource(this._circleSourceId, this._getCircleMapSource()); 1186 | 1187 | map.addLayer(this._getCircleStrokeLayer(), before); 1188 | map.addLayer(this._getCircleFillLayer(), before); 1189 | this._bindCircleFillListeners(map); 1190 | map.on('zoomend', this._onZoomEnd); 1191 | 1192 | if (this.options.editable) { 1193 | map.addSource(this._circleCenterHandleSourceId, this._getCenterHandleMapSource()); 1194 | map.addSource(this._circleRadiusHandlesSourceId, this._getRadiusHandlesMapSource()); 1195 | 1196 | map.addLayer(this._getCircleCenterHandleLayer()); 1197 | this._bindCenterHandleListeners(map); 1198 | 1199 | map.addLayer(this._getCircleRadiusHandlesLayer()); 1200 | this._bindRadiusHandlesListeners(map); 1201 | 1202 | this.on('centerchanged', this._onCenterChanged).on('radiuschanged', this._onRadiusChanged); 1203 | 1204 | MapboxCircle._addActiveEditableCircle(this); 1205 | this._bindBroadcastListeners(); 1206 | } 1207 | 1208 | map.on('styledataloading', this._onMapStyleDataLoading); 1209 | 1210 | const target = map.getContainer(); 1211 | this.observer = new MutationObserver((mutations) => { 1212 | mutations.forEach((mutation) => { 1213 | const removedNodes = Array.from(mutation.removedNodes); 1214 | const directMatch = removedNodes.indexOf(target) > -1; 1215 | const parentMatch = removedNodes.some((parent) => parent.contains(target)); 1216 | if (directMatch || parentMatch) { 1217 | this.remove(); 1218 | } 1219 | }); 1220 | }); 1221 | 1222 | let config = { 1223 | subtree: true, 1224 | childList: true 1225 | }; 1226 | this.observer.observe(document.body, config); 1227 | this.map = map; 1228 | this.zoom = map.getZoom(); 1229 | this._eventEmitter.emit('rendered', this); 1230 | }; 1231 | 1232 | // noinspection JSUnresolvedVariable 1233 | if (map._loaded) { 1234 | if (map.isStyleLoaded()) { 1235 | addCircleAssetsOnMap(); 1236 | } else { 1237 | map.once('render', addCircleAssetsOnMap); 1238 | } 1239 | } else { 1240 | map.once('load', addCircleAssetsOnMap); 1241 | } 1242 | 1243 | return this; 1244 | } 1245 | 1246 | /** 1247 | * Remove source data, layers and listeners from map. 1248 | * @return {MapboxCircle} 1249 | * @public 1250 | */ 1251 | remove() { 1252 | this.map.off('styledataloading', this._onMapStyleDataLoading); 1253 | 1254 | this.observer.disconnect(); 1255 | 1256 | if (this.options.editable) { 1257 | this._unbindBroadcastListeners(); 1258 | MapboxCircle._removeActiveEditableCircle(this); 1259 | 1260 | this.off('radiuschanged', this._onRadiusChanged).off('centerchanged', this._onCenterChanged); 1261 | 1262 | this._unbindRadiusHandlesListeners(); 1263 | if (this.map.getLayer(this._circleRadiusHandlesId)) { 1264 | this.map.removeLayer(this._circleRadiusHandlesId); 1265 | } 1266 | 1267 | this._unbindCenterHandleListeners(); 1268 | if (this.map.getLayer(this._circleCenterHandleId)) { 1269 | this.map.removeLayer(this._circleCenterHandleId); 1270 | } 1271 | 1272 | if (this.map.getSource(this._circleRadiusHandlesSourceId)) { 1273 | this.map.removeSource(this._circleRadiusHandlesSourceId); 1274 | } 1275 | 1276 | if (this.map.getSource(this._circleCenterHandleSourceId)) { 1277 | this.map.removeSource(this._circleCenterHandleSourceId); 1278 | } 1279 | } 1280 | 1281 | this.map.off('zoomend', this._onZoomEnd); 1282 | this._unbindCircleFillListeners(); 1283 | if (this.map.getLayer(this._circleFillId)) { 1284 | this.map.removeLayer(this._circleFillId); 1285 | } 1286 | if (this.map.getLayer(this._circleStrokeId)) { 1287 | this.map.removeLayer(this._circleStrokeId); 1288 | } 1289 | 1290 | if (this.map.getSource(this._circleSourceId)) { 1291 | this.map.removeSource(this._circleSourceId); 1292 | } 1293 | 1294 | this.map = null; 1295 | 1296 | return this; 1297 | } 1298 | 1299 | /** 1300 | * @return {{lat: number, lng: number}} Circle center position 1301 | * @public 1302 | */ 1303 | getCenter() { 1304 | return {lat: this.center[1], lng: this.center[0]}; 1305 | } 1306 | 1307 | /** 1308 | * @param {{lat: number, lng: number}} position 1309 | * @return {MapboxCircle} 1310 | * @public 1311 | */ 1312 | setCenter(position) { 1313 | const applyUpdate = () => { 1314 | this.center = [position.lng, position.lat]; 1315 | if (this.center[0] !== this._lastCenterLngLat[0] && this.center[1] !== this._lastCenterLngLat[1]) { 1316 | this._eventEmitter.emit('centerchanged', this); 1317 | } 1318 | }; 1319 | 1320 | if (this.map) { 1321 | applyUpdate(); 1322 | } else { 1323 | this.on('rendered', applyUpdate, true); 1324 | } 1325 | 1326 | return this; 1327 | } 1328 | 1329 | /** 1330 | * @return {number} Current radius, in meters 1331 | * @public 1332 | */ 1333 | getRadius() { 1334 | return this.radius; 1335 | } 1336 | 1337 | /** 1338 | * @param {number} newRadius Meter radius 1339 | * @return {MapboxCircle} 1340 | * @public 1341 | */ 1342 | setRadius(newRadius) { 1343 | newRadius = Math.round(newRadius); 1344 | const applyUpdate = () => { 1345 | this.radius = newRadius; 1346 | if (this._lastRadius !== newRadius && this.radius === newRadius) { // `this.radius =` subject to min/max lim 1347 | this._eventEmitter.emit('radiuschanged', this); 1348 | } 1349 | }; 1350 | 1351 | if (this.map) { 1352 | applyUpdate(); 1353 | } else { 1354 | this.on('rendered', applyUpdate, true); 1355 | } 1356 | 1357 | return this; 1358 | } 1359 | 1360 | /** 1361 | * @return {{sw: {lat: number, lng: number}, ne: {lat: number, lng: number}}} Southwestern/northeastern bounds 1362 | * @public 1363 | */ 1364 | getBounds() { 1365 | const bboxPolyCoordinates = turfTruncate(turfBboxPoly(turfBbox(this._circle)), 6).geometry.coordinates[0]; 1366 | return { 1367 | sw: {lat: bboxPolyCoordinates[0][1], lng: bboxPolyCoordinates[0][0]}, 1368 | ne: {lat: bboxPolyCoordinates[2][1], lng: bboxPolyCoordinates[2][0]} 1369 | }; 1370 | } 1371 | } 1372 | 1373 | MapboxCircle.__MONOSTATE = { 1374 | instanceIdCounter: 0, 1375 | activeEditableCircles: [], 1376 | broadcast: new EventEmitter() 1377 | }; 1378 | 1379 | module.exports = exports = MapboxCircle; 1380 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapbox-gl-circle", 3 | "version": "1.6.7", 4 | "author": "Smith Micro Software, Inc.", 5 | "license": "ISC", 6 | "description": "A google.maps.Circle replacement for Mapbox GL JS API", 7 | "homepage": "https://github.com/smithmicro/mapbox-gl-circle#readme", 8 | "bugs": { 9 | "url": "https://github.com/smithmicro/mapbox-gl-circle/issues" 10 | }, 11 | "main": "lib/main.js", 12 | "scripts": { 13 | "start": "budo example/index.js --live --force-default-index --title budo/mapbox-gl-circle --verbose -- -t brfs", 14 | "watchify": "mkdir -p dist && watchify lib/main.js -o dist/mapbox-gl-circle-${BUILD_VERSION:-dev}.js --debug -v", 15 | "browserify": "mkdir -p dist && browserify lib/main.js -o dist/mapbox-gl-circle-${BUILD_VERSION:-dev}.js --debug --delay=0 -v", 16 | "prepare": "mkdir -p dist && browserify --standalone MapboxCircle -t [ babelify --presets [ es2015 ] ] lib/main.js | uglifyjs -c -m > dist/mapbox-gl-circle-${BUILD_VERSION:-dev}.min.js && cp -f dist/mapbox-gl-circle-${BUILD_VERSION:-dev}.min.js dist/mapbox-gl-circle.min.js", 17 | "docs": "documentation lint lib/main.js && documentation readme lib/main.js --access public --section=Usage", 18 | "lint": "eslint lib" 19 | }, 20 | "browserify": { 21 | "transform": [ 22 | "babelify" 23 | ] 24 | }, 25 | "files": [ 26 | "lib/", 27 | "example/", 28 | "dist/" 29 | ], 30 | "directories": { 31 | "example": "example", 32 | "lib": "lib" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+ssh://git@github.com/smithmicro/mapbox-gl-circle.git" 37 | }, 38 | "keywords": [ 39 | "mapbox", 40 | "circle", 41 | "osm", 42 | "gl" 43 | ], 44 | "engines": { 45 | "node": ">=7.6.0", 46 | "npm": ">=5.3.0" 47 | }, 48 | "devDependencies": { 49 | "async-each": "^1.0.1", 50 | "babel-preset-es2015": "^6.24.1", 51 | "babelify": "^7.3.0", 52 | "brfs": "^1.4.4", 53 | "browserify": "^14.5.0", 54 | "buble": "^0.15.2", 55 | "budo": "^10.0.4", 56 | "documentation": "^5.1.0", 57 | "eslint": "^4.18.1", 58 | "eslint-config-google": "^0.9.1", 59 | "esutils": "^2.0.2", 60 | "magic-string": "^0.22.4", 61 | "uglify-js": "^3.3.12", 62 | "vlq": "^0.2.3", 63 | "watchify": "^3.10.0" 64 | }, 65 | "dependencies": { 66 | "@turf/bbox": "^4.7.3", 67 | "@turf/bbox-polygon": "^4.7.3", 68 | "@turf/bearing": "^4.5.2", 69 | "@turf/circle": "^4.7.3", 70 | "@turf/destination": "^4.7.3", 71 | "@turf/distance": "^4.7.3", 72 | "@turf/helpers": "^4.7.3", 73 | "@turf/truncate": "^4.7.3", 74 | "events": "^1.1.1", 75 | "lodash": "^4.17.5", 76 | "lodash.debounce": "^4.0.8", 77 | "mapbox-gl": "^0.44.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /prepare-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # Always. 4 | 5 | VERSION_CAT=$1 6 | 7 | if [ -z "$VERSION_CAT" ]; then 8 | echo "Error: Provide a version for 'npm version'; major | minor | patch | prerelease" && exit 1 9 | fi 10 | 11 | echo "Current git version: $(npm version from-git)" 12 | 13 | NEXT_VERSION=$(npm version "$VERSION_CAT") 14 | NEXT_VERSION=${NEXT_VERSION/v/} # Drop the "v" from e.g. "v1.8.0". 15 | echo "Next $VERSION_CAT version: $NEXT_VERSION" 16 | 17 | if [[ ! $VERSION_CAT =~ ^pre ]]; then 18 | echo "" 19 | echo "TODO:" 20 | echo "- Add/update the '### v. $NEXT_VERSION' heading at the top of the README changelog and commit" 21 | fi 22 | 23 | --------------------------------------------------------------------------------