├── .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 | [](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/lint-and-build.yml)
4 | [](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-prerelease.yml)
5 | [](https://github.com/smithmicro/mapbox-gl-circle/actions/workflows/publish-release.yml)
6 | [](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 |
--------------------------------------------------------------------------------