├── .babelrc
├── .dependabot
└── config.yml
├── .editorconfig
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── .mocharc.json
├── .npmignore
├── .nycrc
├── .prettierrc
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── _config.yml
├── build
└── index.js
├── esdoc.json
├── examples
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon-32x32.png
│ ├── index.html
│ └── manifest.json
└── src
│ ├── AllInOne
│ └── index.jsx
│ ├── App.css
│ ├── App.jsx
│ ├── App.test.js
│ ├── CustomMarkerOnOver
│ └── index.jsx
│ ├── MapWithCircle
│ └── index.jsx
│ ├── MapWithMarker
│ └── index.jsx
│ ├── MapWithMovingMarker
│ └── index.jsx
│ ├── MapWithNavigationControlsPosition
│ └── index.jsx
│ ├── MarkerWithPopup
│ └── index.jsx
│ ├── SimpleMap
│ └── index.jsx
│ ├── index.css
│ └── index.js
├── package-lock.json
├── package.json
├── sample.png
├── src
├── Circle
│ ├── index.jsx
│ └── index.test.jsx
├── Diagnose
│ └── index.js
├── Helpers
│ ├── index.js
│ └── index.test.js
├── Lib
│ └── index.js
├── MapboxMap
│ ├── index.css
│ ├── index.jsx
│ └── index.test.jsx
├── Marker
│ ├── index.jsx
│ └── index.test.jsx
├── Popup
│ ├── index.jsx
│ └── index.test.jsx
├── Utils
│ ├── index.js
│ └── index.test.js
├── index.js
└── test.js
├── tools
├── hooks
│ └── pre-commit
└── install_hooks.sh
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": [
4 | [
5 | "babel-plugin-module-resolver",
6 | {
7 | "root": ["./src"]
8 | }
9 | ],
10 | "@babel/plugin-proposal-class-properties"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.dependabot/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | # https://dependabot.com/docs/config-file/
4 |
5 | version: 1
6 | update_configs:
7 | - package_manager: "javascript"
8 | directory: "/"
9 | target_branch: "dependencies_update"
10 | update_schedule: "weekly"
11 | commit_message:
12 | prefix: "chore"
13 | include_scope: true
14 | automerged_updates:
15 | - match:
16 | dependency_type: "all"
17 | update_type: "all"
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{js,jsx,json}]
12 | indent_size = 4
13 |
14 | [Makefile]
15 | indent_style = tabs
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "prettier", "prettier/react"],
3 | "parser": "babel-eslint",
4 | "parserOptions": {
5 | "ecmaFeatures": {
6 | "jsx": true
7 | }
8 | },
9 | "settings": {
10 | "import/resolver": {
11 | "webpack": {}
12 | }
13 | },
14 | "env": {
15 | "browser": true,
16 | "node": true,
17 | "mocha": true
18 | },
19 | "globals": {
20 | "Promise": true,
21 | "expect": true
22 | },
23 | "plugins": ["babel", "react"],
24 | "rules": {
25 | "react/jsx-props-no-spreading": 0,
26 | "max-classes-per-file": 0
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Device (please complete the following information):**
24 | - Device if applicable: [e.g. iPhone6]
25 | - OS: [e.g. iOS]
26 | - Browser [e.g. chrome, safari]
27 | - Version [e.g. 22]
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Build Package
4 |
5 | on:
6 | push:
7 | branches:
8 | - master
9 | pull_request:
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v1
16 | - uses: actions/setup-node@v1
17 | with:
18 | node-version: 13
19 | - run: npm ci
20 | - run: npm run peers
21 | - run: npm run ci
22 | env:
23 | CODECOV_TOKEN: ${{secrets.codecov_token}}
24 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Publish Package
4 |
5 | on:
6 | push:
7 | tags:
8 | - v[0-9]+\.[0-9]+\.[0-9]+
9 | paths:
10 | - package.json
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v1
17 | - uses: actions/setup-node@v1
18 | with:
19 | node-version: 13
20 | registry-url: https://registry.npmjs.org/
21 | - run: npm publish
22 | env:
23 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | esdoc/
3 | coverage/
4 | .nyc_output/
5 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "diff": true,
3 | "extension": ["js", "jsx"],
4 | "file": "src/test.js",
5 | "full-trace": true,
6 | "recursive": true,
7 | "require": ["@babel/register", "ignore-styles"]
8 | }
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # gitignore
2 | node_modules/
3 | esdoc/
4 | coverage/
5 | .nyc_output/
6 |
7 | # npmignore
8 | examples/
9 | src/
10 | _config.yml
11 | .babelrc
12 | .eslintrc
13 | .nycrc
14 | .prettierrc
15 | .travis.yml
16 | CODE_OF_CONDUCT.md
17 | esdoc.json
18 | mocha.opts
19 | sample.png
20 | webpack.config.js
21 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "reporter": [
3 | "text",
4 | "lcov"
5 | ],
6 | "require": [
7 | "@babel/register"
8 | ],
9 | "extension": [
10 | ".jsx"
11 | ],
12 | "include": [
13 | "src/**/*"
14 | ],
15 | "exclude": [
16 | "**/*webpack*",
17 | "src/index.js",
18 | "src/Diagnose/",
19 | "src/**/*test.js",
20 | "src/**/*test.jsx",
21 | "examples/**"
22 | ],
23 | "check-coverage": true,
24 | "lines": 80,
25 | "statements": 80,
26 | "functions": 80,
27 | "branches": 80
28 | }
29 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "tabWidth": 4
6 | }
7 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @MeilleursAgents/web-front
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at geeks+mapbox@meilleursagents.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 MeilleursAgents.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Before creating my pull-request, I ensure:
2 |
3 | - [ ] Javascript documentation is written
4 | - [ ] Unit tests are written and green
5 | - [ ] I've cleaned my WIP or meaningless commits
6 | - [ ] Examples and documentation site is up-to-date
7 |
8 | # In order to validate the pull request, I do:
9 |
10 | - *List steps to help reviewers*
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-mapbox-wrapper
2 |
3 | [](https://travis-ci.org/MeilleursAgents/react-mapbox-wrapper)
4 | [](https://badge.fury.io/js/react-mapbox-wrapper)
5 | [](https://codecov.io/gh/MeilleursAgents/react-mapbox-wrapper)
6 |
7 | React wrapper for [mapboxgl-js API](https://www.mapbox.com/mapbox-gl-js/api/).
8 |
9 | Made with ❤️ by [MeilleursAgents](https://www.meilleursagents.com)
10 |
11 | 
12 |
13 | ## Usage
14 |
15 | ```js
16 | import React from 'react';
17 | import MapboxMap from 'react-mapbox-wrapper';
18 |
19 | export default function SimpleMap() {
20 | return (
21 |
22 |
26 |
27 | );
28 | }
29 |
30 | SimpleMap.displayName = 'SimpleMap';
31 | ```
32 |
33 | See [examples](examples/src/) folder for more example.
34 |
35 | ## Getting started
36 |
37 | Read carefully the [getting started section](https://www.mapbox.com/mapbox-gl-js/api/) of Mapbox GL API.
38 |
39 | If you use Webpack, you should add the following parameter in order to properly build your app.
40 |
41 | ```js
42 | module: {
43 | noParse: /(mapbox-gl)\.js$/,
44 | }
45 | ```
46 |
47 | `react-mapbox-wrapper` import the corresponding CSS stylesheet from Javascript, you don't have to do it.
48 |
49 | ## API
50 |
51 | ### MapboxMap
52 |
53 | React Component that render a Mapbox Map. Extra props are directly passed to the [Map constructor](https://www.mapbox.com/mapbox-gl-js/api/#map). Following props are handled by wrapper for updating or handling behavior in the React philosophy.
54 |
55 | Wrapper is CSS flex-ready for _width_ but you **have to set a height** for having visible Mapbox.
56 |
57 | | Props | Type | Default | Description |
58 | | ------------------------- | ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59 | | accessToken | String | required | Mapbox Access Token ([find it here](https://www.mapbox.com/account/access-tokens)) |
60 | | coordinates | Object | required | Coordinates of the map center According to the [LngLatLike](https://www.mapbox.com/mapbox-gl-js/api/#lnglatlike) model |
61 | | className | string | `''` | `className` added to map's wrapper. Your should provide a height in order to render the map (default at 0) |
62 | | children | Node | `null` | Rendered children, typically a [Marker](#Marker) and/or [Circle](#Circle) |
63 | | minZoom | number | `undefined` | The minimum zoom level of the map (0-24). |
64 | | maxZoom | number | `undefined` | The maximum zoom level of the map (0-24). |
65 | | zoom | number | `15` | The viewport zoom |
66 | | onChange | func | `undefined` | Callback function called on every viewport change (`moveend`, `zoomend`) Callback receives params as first argument with the following shape `{ zoom: 15, coordinates: { lng: 3.4, lat: 1.2 } }` and [Mapbox Map object](https://www.mapbox.com/mapbox-gl-js/api/#map) as second argument |
67 | | onClick | func | `undefined` | Callback function called on [map's click](https://www.mapbox.com/mapbox-gl-js/api/#map.event:click) |
68 | | onLoad | func | `undefined` | Callback function called on [map's load](https://www.mapbox.com/mapbox-gl-js/api/#map.event:load) with current Mapbox instance param |
69 | | onZoomStart | func | `undefined` | Callback function called on [map's zoomstart](https://www.mapbox.com/mapbox-gl-js/api/#map.event:zoomstart) |
70 | | onZoomEnd | func | `undefined` | Callback function called on [map's zoomend](https://www.mapbox.com/mapbox-gl-js/api/#map.event:zoomend) with a debounce of 300ms in order to avoid too many `render()` call Callback receives event object as first argument and [Mapbox Map object](https://www.mapbox.com/mapbox-gl-js/api/#map) as second argument |
71 | | renderNotSupported | func | _Simple message_ | Callback function called when [browser does not support mapbox-gl](https://www.mapbox.com/mapbox-gl-js/api/#supported) |
72 | | mapboxStyle | String | | [Mapbox style layer](https://www.mapbox.com/mapbox-gl-js/style-spec/) |
73 | | withCompass | bool | `false` | Show compass [Navigation Control](https://www.mapbox.com/mapbox-gl-js/api/#navigationcontrol) |
74 | | withFullscreen | bool | `false` | Show [Fullscreen Control](https://www.mapbox.com/mapbox-gl-js/api/#fullscreencontrol) |
75 | | withZoom | bool | `false` | Show zoom [Navigation Control](https://www.mapbox.com/mapbox-gl-js/api/#navigationcontrol) |
76 | | fullscreenControlPosition | string | `top-right` | Set [Fullscreen Control](https://www.mapbox.com/mapbox-gl-js/api/#fullscreencontrol) position |
77 | | navigationControlPosition | string | `bottom-right` | Set [Navigation Control](https://www.mapbox.com/mapbox-gl-js/api/#navigationcontrol) position |
78 | | navigationType | Object | `{type: "jumpTo"}` | Set type of navigation when changing coordinates: `flyTo`, `easeTo` or `jumpTo`. You can change speed and curve options for type `flyTo` as follows: `{type: "flyTo", options: {curve: 1.42, speed: 1.2}}` |
79 |
80 | ### Marker
81 |
82 | React Component that render a Marker. Extra props are directly passed to the [Marker constructor](https://www.mapbox.com/mapbox-gl-js/api/#marker)
83 |
84 | | Props | Type | Default | Description |
85 | | ---------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
86 | | children | Node | `null` | Marker HTML DOM, default marker will be used if not provided |
87 | | coordinates | Object | required | Coordinates of the marker According to the [LngLatLike](https://www.mapbox.com/mapbox-gl-js/api/#lnglatlike) model |
88 | | draggable | bool | `false` | Allow user to drag'n'drop Marker |
89 | | getRef | func | | Callback function called with marker's ref (useful for calling `#moveToTop()` function) |
90 | | map | Object | required | Mapbox Map where marker will be added (can be obtained with MapboxMap#onLoad props fn) |
91 | | onDragEnd | func | | Callback function called on [marker's dragend](https://www.mapbox.com/mapbox-gl-js/api/#marker.event:dragend) |
92 | | onMouseOut | func | | Callback function called on marker or popup mouseOut |
93 | | onMouseOver | func | | Callback function called on marker or popup mouseOver |
94 | | popup | Node | | Popup attached to the marker, displayed on click to marker |
95 | | popupAnchor | string | `bottom` | [Popup anchor param](https://www.mapbox.com/mapbox-gl-js/api/#popup) |
96 | | popupCloseButton | bool | `false` | [Popup closeButton param](https://www.mapbox.com/mapbox-gl-js/api/#popup) |
97 | | popupOffset | number | | [Popup offset param](https://www.mapbox.com/mapbox-gl-js/api/#popup) |
98 | | popupOnOver | bool | `false` | Trigger popup show on mouse over (only available if **children** are provided, default marker cannot be bind) |
99 |
100 | ### Circle
101 |
102 | React Component that render a Circle. Extra props are directly passed to the [Marker constructor](https://www.mapbox.com/mapbox-gl-js/api/#marker)
103 |
104 | | Props | Type | Default | Description |
105 | | ----------- | ------ | ------------ | ------------------------------------------------------------------------------------------------------------------------- |
106 | | coordinates | Object | required | Coordinates of the marker According to the [LngLatLike](https://www.mapbox.com/mapbox-gl-js/api/#lnglatlike) model |
107 | | id | string | required | Identifier of circle, used to name the underlying layer |
108 | | map | Object | required | Mapbox Map where marker will be added (can be obtained with MapboxMap#onLoad props fn) |
109 | | onClick | func | | Callback function called on circle's click |
110 | | paint | Object | | [Paint option of the layer](https://www.mapbox.com/mapbox-gl-js/style-spec#layer-paint) |
111 | | radius | number | required | Radius of circle, in kilometers |
112 | | unit | string | `kilometers` | Unit of the radius. values can be : `kilometers`, `meters`, `miles`, `feet` |
113 |
114 | ### Helpers
115 |
116 | We provide some [helpers](https://github.com/MeilleursAgents/react-mapbox-wrapper/blob/master/src/Helpers/index.js) for interacting with Mapbox Map object, all in `Helpers` import.
117 |
118 | ```js
119 | import { Helpers } from react-mapbox-wrapper;
120 |
121 | Helpers.convertRadiusUnit(580, 'meters');
122 | // > { radius: 0.58, unit: 'kilometers' }
123 |
124 | Helpers.getCircleData({ lat: 48.868526, lng: 2.3434886 }, 800, 'meters')
125 | // > Coordinates for drawing a circle with a 800meters radius at given coordinates
126 |
127 | Helpers.coordinatesAreEqual({ lat: 48.868526, lng: 2.3434886 }, [2.3434886, 48.868526]);
128 | // > Check if given coordinates are equal, even in different format
129 |
130 | Helpers.newBounds(coordinatesSouthWest, coordinatesNorthEast);
131 | // > Create a new LngLatBounds from Mapbox library
132 |
133 | Helpers.newBound(coordinates);
134 | // > Create a new LngLatLike from Mapbox library
135 |
136 | Helpers.drawGeoJSON(map, 'foodTruck', data, { 'fill-opacity': 0.2 }, undefined, 'fill');
137 | // > Draw a geoJSON with given data and type. Can be used to also update geoJSON if layer already exist
138 |
139 | Helpers.removeGeoJSON(map, 'foodTruck');
140 | // > Remove the layer previously added with #addGeoJSON method
141 | ```
142 |
143 | ## Development
144 |
145 | ```bash
146 | npm install && npm run peers # install both dependencies and peers
147 | ```
148 |
149 | You can use [`npm link`](https://docs.npmjs.com/cli/link) while developing new features on this repo for use in targeted repository.
150 |
151 | ```bash
152 | npm link
153 | ```
154 |
155 | ### Git hooks
156 |
157 | You can use our `pre-commit` hook for ensuring consistent format and linting before committing by running command:
158 |
159 | ```bash
160 | ./tools/install_hooks.sh
161 | ```
162 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | theme: jekyll-theme-cayman
4 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | module.exports=(()=>{"use strict";var n={133:(n,e,t)=>{t.r(e),t.d(e,{Circle:()=>H,Diagnose:()=>wn,Helpers:()=>L,Marker:()=>bn,Popup:()=>tn,Utils:()=>b,default:()=>Cn,mapboxgl:()=>m});const o=require("react");var r=t.n(o);const a=require("prop-types");var i=t.n(a);const c=require("@turf/circle");var l=t.n(c);function p(n){return(p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}function s(n,e){if(n===e)return!0;if(p(n)!==p(e))return!1;var t=p(n);if(t!==p(e))return!1;if(null===n&&null!==e||null!==n&&null===e)return!1;if("number"===t&&Number.isNaN(n)&&Number.isNaN(e))return!0;if("undefined"===t||"string"===t||"number"===t||"boolean"===t)return!1;if(n instanceof Date&&e instanceof Date)return n.getTime()===e.getTime();if(Array.isArray(n)){if(n.length!==e.length)return!1;for(var o=0,r=n.length;o1&&void 0!==arguments[1]?arguments[1]:"kilometers",o=Number(n),r=e;return Number.isFinite(n)||t.g.console.error("The radius given is not a number"),-1===d.indexOf(e)&&(r=d[0],t.g.console.warn('The unit "'.concat(e,'" is not supported, the fallback "').concat(r,'" is used'))),"meters"===e?(o=n/1e3,r="kilometers"):"feet"===e&&(o=n/5280,r="miles"),{radius:o,unit:r}}function v(n){return Array.isArray(n)&&2===n.length?{lng:n[0],lat:n[1]}:n instanceof Object&&null!==n?{lng:n.lng||n.lon,lat:n.lat}:{}}function y(n,e){var t=v(n),o=v(e);return t.lat===o.lat&&t.lng===o.lng}function x(n){var e=v(n);return[e.lng,e.lat]}function w(n,e,t){var o=h(e,t),r=o.radius,a=o.unit;return l()(x(n),r,{steps:64,units:a})}function C(n){return"".concat(n,"_layer")}function k(n,e,t){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},r=arguments.length>4?arguments[4]:void 0,a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"fill";if(e&&n&&t){var i=C(e),c=n.getSource(e);c?(c.setData(t),Object.keys(o).forEach((function(e){return n.setPaintProperty(i,e,o[e])}))):(n.addSource(e,{type:"geojson",data:t}),n.addLayer({id:i,type:a,source:e,layout:{},paint:o}),u(r)&&(n.on("click",i,r),n.on("mouseenter",i,(function(){n.getCanvas().style.cursor="pointer"})),n.on("mouseleave",i,(function(){n.getCanvas().style.cursor=""}))))}}function O(n,e){if(e&&n){var o=C(e);try{n.getLayer(o)&&n.removeLayer(o)}catch(n){t.g.console.warn("Error while removing GeoJSON layer with id ".concat(o,". It's sometimes due to an already removed map"),n)}try{n.getSource(e)&&n.removeSource(e)}catch(n){t.g.console.warn("Error while removing GeoJSON soruce with id ".concat(e,". It's sometimes due to an already removed map"),n)}}}const L={convertRadiusUnit:h,coordinatesAreEqual:y,drawGeoJSON:k,getCircleData:w,getLayerId:C,newBound:x,newBounds:function(n,e){return new m.LngLatBounds(n,e)},removeGeoJSON:O};var E=t(379),M=t.n(E),S=t(113);M()(S.Z,{insert:"head",singleton:!1}),S.Z.locals;var j=t(323);function z(n){return(z="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}function P(n,e){var t=Object.keys(n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(n);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),t.push.apply(t,o)}return t}function T(n,e,t){return e in n?Object.defineProperty(n,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):n[e]=t,n}function _(n,e){for(var t=0;t=0){var m,g,d={center:[t.lng,t.lat]};"flyTo"===b&&(d.curve=(null==c||null===(m=c.options)||void 0===m?void 0:m.curve)||1.42,d.speed=(null==c||null===(g=c.options)||void 0===g?void 0:g.speed)||1.2),this.map[b](d)}}o!==p&&this.map.setZoom(o),r!==s&&this.map.setMinZoom(r),a!==u&&this.map.setMaxZoom(a),i!==f&&this.map.setStyle(i)}}},{key:"componentWillUnmount",value:function(){this.map&&this.map.remove()}},{key:"onChange",value:function(){var n=this.props.onChange;if(u(n)){var e=this.map.getCenter(),t=e.lng,o=e.lat;n({zoom:this.map.getZoom(),coordinates:{lng:t,lat:o}},this.map)}}},{key:"onZoomEnd",value:function(n){var e=this;clearTimeout(this.zoomendTimeout),this.zoomendTimeout=setTimeout((function(){e.onChange();var t=e.props.onZoomEnd;u(t)&&t(n,e.map)}),300)}},{key:"initMap",value:function(n){var e=this;if(n){var t=this.props,o=t.coordinates,r=t.onLoad,a=t.mapboxStyle,i=function(n,e){if(null==n)return{};var t,o,r=function(n,e){if(null==n)return{};var t,o,r={},a=Object.keys(n);for(o=0;o=0||(r[t]=n[t]);return r}(n,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(n);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(n,t)&&(r[t]=n[t])}return r}(t,["coordinates","onLoad","mapboxStyle"]);this.map=new m.Map(function(n){for(var e=1;e=0||(r[t]=n[t]);return r}(n,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(n);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(n,t)&&(r[t]=n[t])}return r}(e,["coordinates","map","draggable","onDragEnd","getRef","children"]);this.marker=new m.Marker(function(n){for(var e=1;e{t.d(e,{Z:()=>a});var o=t(645),r=t.n(o)()(!1);r.push([n.id,".mapboxgl-map {\n font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;\n overflow: hidden;\n position: relative;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\n.mapboxgl-map:-webkit-full-screen {\n width: 100%;\n height: 100%;\n}\n\n.mapboxgl-canary {\n background-color: salmon;\n}\n\n.mapboxgl-canvas-container.mapboxgl-interactive,\n.mapboxgl-ctrl-group > button.mapboxgl-ctrl-compass {\n cursor: -webkit-grab;\n cursor: -moz-grab;\n cursor: grab;\n -moz-user-select: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.mapboxgl-canvas-container.mapboxgl-interactive:active,\n.mapboxgl-ctrl-group > button.mapboxgl-ctrl-compass:active {\n cursor: -webkit-grabbing;\n cursor: -moz-grabbing;\n cursor: grabbing;\n}\n\n.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,\n.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas {\n touch-action: pan-x pan-y;\n}\n\n.mapboxgl-canvas-container.mapboxgl-touch-drag-pan,\n.mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas {\n touch-action: pinch-zoom;\n}\n\n.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,\n.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas {\n touch-action: none;\n}\n\n.mapboxgl-ctrl-top-left,\n.mapboxgl-ctrl-top-right,\n.mapboxgl-ctrl-bottom-left,\n.mapboxgl-ctrl-bottom-right { position: absolute; pointer-events: none; z-index: 2; }\n.mapboxgl-ctrl-top-left { top: 0; left: 0; }\n.mapboxgl-ctrl-top-right { top: 0; right: 0; }\n.mapboxgl-ctrl-bottom-left { bottom: 0; left: 0; }\n.mapboxgl-ctrl-bottom-right { right: 0; bottom: 0; }\n\n.mapboxgl-ctrl { clear: both; pointer-events: auto; }\n.mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin: 10px 0 0 10px; float: left; }\n.mapboxgl-ctrl-top-right .mapboxgl-ctrl { margin: 10px 10px 0 0; float: right; }\n.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl { margin: 0 0 10px 10px; float: left; }\n.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl { margin: 0 10px 10px 0; float: right; }\n\n.mapboxgl-ctrl-group {\n border-radius: 4px;\n -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\n -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\n box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n background: #fff;\n}\n\n.mapboxgl-ctrl-group > button {\n width: 30px;\n height: 30px;\n display: block;\n padding: 0;\n outline: none;\n border: 0;\n box-sizing: border-box;\n background-color: transparent;\n cursor: pointer;\n}\n\n.mapboxgl-ctrl-group > button + button {\n border-top: 1px solid #ddd;\n}\n\n/* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */\n.mapboxgl-ctrl > button::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n.mapboxgl-ctrl > button:hover {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.mapboxgl-ctrl-icon,\n.mapboxgl-ctrl-icon > .mapboxgl-ctrl-compass-arrow {\n speak: none;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n.mapboxgl-ctrl-icon {\n padding: 5px;\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23333333;' d='m 7,9 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 l 6,0 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23333333;' d='M 10 6 C 9.446 6 9 6.4459904 9 7 L 9 9 L 7 9 C 6.446 9 6 9.446 6 10 C 6 10.554 6.446 11 7 11 L 9 11 L 9 13 C 9 13.55401 9.446 14 10 14 C 10.554 14 11 13.55401 11 13 L 11 11 L 13 11 C 13.554 11 14 10.554 14 10 C 14 9.446 13.554 9 13 9 L 11 9 L 11 7 C 11 6.4459904 10.554 6 10 6 z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate:disabled {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E %3Cpath d='M 10,4 C 9,4 9,5 9,5 L 9,5.1 C 7.0357113,5.5006048 5.5006048,7.0357113 5.1,9 L 5,9 c 0,0 -1,0 -1,1 0,1 1,1 1,1 l 0.1,0 c 0.4006048,1.964289 1.9357113,3.499395 3.9,3.9 L 9,15 c 0,0 0,1 1,1 1,0 1,-1 1,-1 l 0,-0.1 c 1.964289,-0.400605 3.499395,-1.935711 3.9,-3.9 l 0.1,0 c 0,0 1,0 1,-1 C 16,9 15,9 15,9 L 14.9,9 C 14.499395,7.0357113 12.964289,5.5006048 11,5.1 L 11,5 c 0,0 0,-1 -1,-1 z m 0,2.5 c 1.932997,0 3.5,1.5670034 3.5,3.5 0,1.932997 -1.567003,3.5 -3.5,3.5 C 8.0670034,13.5 6.5,11.932997 6.5,10 6.5,8.0670034 8.0670034,6.5 10,6.5 Z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E %3Cpath d='M 10,4 C 9,4 9,5 9,5 L 9,5.1 C 7.0357113,5.5006048 5.5006048,7.0357113 5.1,9 L 5,9 c 0,0 -1,0 -1,1 0,1 1,1 1,1 l 0.1,0 c 0.4006048,1.964289 1.9357113,3.499395 3.9,3.9 L 9,15 c 0,0 0,1 1,1 1,0 1,-1 1,-1 l 0,-0.1 c 1.964289,-0.400605 3.499395,-1.935711 3.9,-3.9 l 0.1,0 c 0,0 1,0 1,-1 C 16,9 15,9 15,9 L 14.9,9 C 14.499395,7.0357113 12.964289,5.5006048 11,5.1 L 11,5 c 0,0 0,-1 -1,-1 z m 0,2.5 c 1.932997,0 3.5,1.5670034 3.5,3.5 0,1.932997 -1.567003,3.5 -3.5,3.5 C 8.0670034,13.5 6.5,11.932997 6.5,10 6.5,8.0670034 8.0670034,6.5 10,6.5 Z'/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting {\n -webkit-animation: mapboxgl-spin 2s infinite linear;\n -moz-animation: mapboxgl-spin 2s infinite linear;\n -o-animation: mapboxgl-spin 2s infinite linear;\n -ms-animation: mapboxgl-spin 2s infinite linear;\n animation: mapboxgl-spin 2s infinite linear;\n}\n\n@-webkit-keyframes mapboxgl-spin {\n 0% { -webkit-transform: rotate(0deg); }\n 100% { -webkit-transform: rotate(360deg); }\n}\n\n@-moz-keyframes mapboxgl-spin {\n 0% { -moz-transform: rotate(0deg); }\n 100% { -moz-transform: rotate(360deg); }\n}\n\n@-o-keyframes mapboxgl-spin {\n 0% { -o-transform: rotate(0deg); }\n 100% { -o-transform: rotate(360deg); }\n}\n\n@-ms-keyframes mapboxgl-spin {\n 0% { -ms-transform: rotate(0deg); }\n 100% { -ms-transform: rotate(360deg); }\n}\n\n@keyframes mapboxgl-spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-fullscreen {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath d='M 5 4 C 4.5 4 4 4.5 4 5 L 4 6 L 4 9 L 4.5 9 L 5.7773438 7.296875 C 6.7771319 8.0602131 7.835765 8.9565728 8.890625 10 C 7.8257121 11.0633 6.7761791 11.951675 5.78125 12.707031 L 4.5 11 L 4 11 L 4 15 C 4 15.5 4.5 16 5 16 L 9 16 L 9 15.5 L 7.2734375 14.205078 C 8.0428931 13.187886 8.9395441 12.133481 9.9609375 11.068359 C 11.042371 12.14699 11.942093 13.2112 12.707031 14.21875 L 11 15.5 L 11 16 L 14 16 L 15 16 C 15.5 16 16 15.5 16 15 L 16 14 L 16 11 L 15.5 11 L 14.205078 12.726562 C 13.177985 11.949617 12.112718 11.043577 11.037109 10.009766 C 12.151856 8.981061 13.224345 8.0798624 14.228516 7.3046875 L 15.5 9 L 16 9 L 16 5 C 16 4.5 15.5 4 15 4 L 11 4 L 11 4.5 L 12.703125 5.7773438 C 11.932647 6.7864834 11.026693 7.8554712 9.9707031 8.9199219 C 8.9584739 7.8204943 8.0698767 6.7627188 7.3046875 5.7714844 L 9 4.5 L 9 4 L 6 4 L 5 4 z '/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-shrink {\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23000000;' d='M 4.2421875 3.4921875 A 0.750075 0.750075 0 0 0 3.71875 4.78125 L 5.9648438 7.0273438 L 4 8.5 L 4 9 L 8 9 C 8.500001 8.9999988 9 8.4999992 9 8 L 9 4 L 8.5 4 L 7.0175781 5.9550781 L 4.78125 3.71875 A 0.750075 0.750075 0 0 0 4.2421875 3.4921875 z M 15.734375 3.4921875 A 0.750075 0.750075 0 0 0 15.21875 3.71875 L 12.984375 5.953125 L 11.5 4 L 11 4 L 11 8 C 11 8.4999992 11.499999 8.9999988 12 9 L 16 9 L 16 8.5 L 14.035156 7.0273438 L 16.28125 4.78125 A 0.750075 0.750075 0 0 0 15.734375 3.4921875 z M 4 11 L 4 11.5 L 5.9648438 12.972656 L 3.71875 15.21875 A 0.75130096 0.75130096 0 1 0 4.78125 16.28125 L 7.0273438 14.035156 L 8.5 16 L 9 16 L 9 12 C 9 11.500001 8.500001 11.000001 8 11 L 4 11 z M 12 11 C 11.499999 11.000001 11 11.500001 11 12 L 11 16 L 11.5 16 L 12.972656 14.035156 L 15.21875 16.28125 A 0.75130096 0.75130096 0 1 0 16.28125 15.21875 L 14.035156 12.972656 L 16 11.5 L 16 11 L 12 11 z '/%3E %3C/svg%3E\");\n}\n\n.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow {\n width: 20px;\n height: 20px;\n margin: 5px;\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpolygon fill='%23333333' points='6,9 10,1 14,9'/%3E %3Cpolygon fill='%23CCCCCC' points='6,11 10,19 14,11 '/%3E %3C/svg%3E\");\n background-repeat: no-repeat;\n display: inline-block;\n}\n\na.mapboxgl-ctrl-logo {\n width: 85px;\n height: 21px;\n margin: 0 0 -3px -3px;\n display: block;\n background-repeat: no-repeat;\n cursor: pointer;\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 84.49 21' style='enable-background:new 0 0 84.49 21;' xml:space='preserve'%3E%3Cg%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M83.25,14.26c0,0.12-0.09,0.21-0.21,0.21h-1.61c-0.13,0-0.24-0.06-0.3-0.17l-1.44-2.39l-1.44,2.39 c-0.06,0.11-0.18,0.17-0.3,0.17h-1.61c-0.04,0-0.08-0.01-0.12-0.03c-0.09-0.06-0.13-0.19-0.06-0.28l0,0l2.43-3.68L76.2,6.84 c-0.02-0.03-0.03-0.07-0.03-0.12c0-0.12,0.09-0.21,0.21-0.21h1.61c0.13,0,0.24,0.06,0.3,0.17l1.41,2.36l1.4-2.35 c0.06-0.11,0.18-0.17,0.3-0.17H83c0.04,0,0.08,0.01,0.12,0.03c0.09,0.06,0.13,0.19,0.06,0.28l0,0l-2.37,3.63l2.43,3.67 C83.24,14.18,83.25,14.22,83.25,14.26z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M66.24,9.59c-0.39-1.88-1.96-3.28-3.84-3.28c-1.03,0-2.03,0.42-2.73,1.18V3.51c0-0.13-0.1-0.23-0.23-0.23h-1.4 c-0.13,0-0.23,0.11-0.23,0.23v10.72c0,0.13,0.1,0.23,0.23,0.23h1.4c0.13,0,0.23-0.11,0.23-0.23V13.5c0.71,0.75,1.7,1.18,2.73,1.18 c1.88,0,3.45-1.41,3.84-3.29C66.37,10.79,66.37,10.18,66.24,9.59L66.24,9.59z M62.08,13c-1.32,0-2.39-1.11-2.41-2.48v-0.06 c0.02-1.38,1.09-2.48,2.41-2.48s2.42,1.12,2.42,2.51S63.41,13,62.08,13z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M71.67,6.32c-1.98-0.01-3.72,1.35-4.16,3.29c-0.13,0.59-0.13,1.19,0,1.77c0.44,1.94,2.17,3.32,4.17,3.3 c2.35,0,4.26-1.87,4.26-4.19S74.04,6.32,71.67,6.32z M71.65,13.01c-1.33,0-2.42-1.12-2.42-2.51s1.08-2.52,2.42-2.52 c1.33,0,2.42,1.12,2.42,2.51S72.99,13,71.65,13.01L71.65,13.01z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M62.08,7.98c-1.32,0-2.39,1.11-2.41,2.48v0.06C59.68,11.9,60.75,13,62.08,13s2.42-1.12,2.42-2.51 S63.41,7.98,62.08,7.98z M62.08,11.76c-0.63,0-1.14-0.56-1.17-1.25v-0.04c0.01-0.69,0.54-1.25,1.17-1.25 c0.63,0,1.17,0.57,1.17,1.27C63.24,11.2,62.73,11.76,62.08,11.76z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M71.65,7.98c-1.33,0-2.42,1.12-2.42,2.51S70.32,13,71.65,13s2.42-1.12,2.42-2.51S72.99,7.98,71.65,7.98z M71.65,11.76c-0.64,0-1.17-0.57-1.17-1.27c0-0.7,0.53-1.26,1.17-1.26s1.17,0.57,1.17,1.27C72.82,11.21,72.29,11.76,71.65,11.76z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M45.74,6.53h-1.4c-0.13,0-0.23,0.11-0.23,0.23v0.73c-0.71-0.75-1.7-1.18-2.73-1.18 c-2.17,0-3.94,1.87-3.94,4.19s1.77,4.19,3.94,4.19c1.04,0,2.03-0.43,2.73-1.19v0.73c0,0.13,0.1,0.23,0.23,0.23h1.4 c0.13,0,0.23-0.11,0.23-0.23V6.74c0-0.12-0.09-0.22-0.22-0.22C45.75,6.53,45.75,6.53,45.74,6.53z M44.12,10.53 C44.11,11.9,43.03,13,41.71,13s-2.42-1.12-2.42-2.51s1.08-2.52,2.4-2.52c1.33,0,2.39,1.11,2.41,2.48L44.12,10.53z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M41.71,7.98c-1.33,0-2.42,1.12-2.42,2.51S40.37,13,41.71,13s2.39-1.11,2.41-2.48v-0.06 C44.1,9.09,43.03,7.98,41.71,7.98z M40.55,10.49c0-0.7,0.52-1.27,1.17-1.27c0.64,0,1.14,0.56,1.17,1.25v0.04 c-0.01,0.68-0.53,1.24-1.17,1.24C41.08,11.75,40.55,11.19,40.55,10.49z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M52.41,6.32c-1.03,0-2.03,0.42-2.73,1.18V6.75c0-0.13-0.1-0.23-0.23-0.23h-1.4c-0.13,0-0.23,0.11-0.23,0.23 v10.72c0,0.13,0.1,0.23,0.23,0.23h1.4c0.13,0,0.23-0.1,0.23-0.23V13.5c0.71,0.75,1.7,1.18,2.74,1.18c2.17,0,3.94-1.87,3.94-4.19 S54.58,6.32,52.41,6.32z M52.08,13.01c-1.32,0-2.39-1.11-2.42-2.48v-0.07c0.02-1.38,1.09-2.49,2.4-2.49c1.32,0,2.41,1.12,2.41,2.51 S53.4,13,52.08,13.01L52.08,13.01z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M52.08,7.98c-1.32,0-2.39,1.11-2.42,2.48v0.06c0.03,1.38,1.1,2.48,2.42,2.48s2.41-1.12,2.41-2.51 S53.4,7.98,52.08,7.98z M52.08,11.76c-0.63,0-1.14-0.56-1.17-1.25v-0.04c0.01-0.69,0.54-1.25,1.17-1.25c0.63,0,1.17,0.58,1.17,1.27 S52.72,11.76,52.08,11.76z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M36.08,14.24c0,0.13-0.1,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68c0-0.98-0.74-1.71-1.62-1.71 c-0.8,0-1.46,0.7-1.59,1.62l0.01,4.66c0,0.13-0.11,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68 c0-0.98-0.74-1.71-1.62-1.71c-0.85,0-1.54,0.79-1.6,1.8v4.48c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V6.74 c0.01-0.13,0.1-0.22,0.23-0.22h1.4c0.13,0,0.22,0.11,0.23,0.22V7.4c0.5-0.68,1.3-1.09,2.16-1.1h0.03c1.09,0,2.09,0.6,2.6,1.55 c0.45-0.95,1.4-1.55,2.44-1.56c1.62,0,2.93,1.25,2.9,2.78L36.08,14.24z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M84.34,13.59l-0.07-0.13l-1.96-2.99l1.94-2.95c0.44-0.67,0.26-1.56-0.41-2.02c-0.02,0-0.03,0-0.04-0.01 c-0.23-0.15-0.5-0.22-0.78-0.22h-1.61c-0.56,0-1.08,0.29-1.37,0.78L79.72,6.6l-0.34-0.56C79.09,5.56,78.57,5.27,78,5.27h-1.6 c-0.6,0-1.13,0.37-1.35,0.92c-2.19-1.66-5.28-1.47-7.26,0.45c-0.35,0.34-0.65,0.72-0.89,1.14c-0.9-1.62-2.58-2.72-4.5-2.72 c-0.5,0-1.01,0.07-1.48,0.23V3.51c0-0.82-0.66-1.48-1.47-1.48h-1.4c-0.81,0-1.47,0.66-1.47,1.47v3.75 c-0.95-1.36-2.5-2.18-4.17-2.19c-0.74,0-1.46,0.16-2.12,0.47c-0.24-0.17-0.54-0.26-0.84-0.26h-1.4c-0.45,0-0.87,0.21-1.15,0.56 c-0.02-0.03-0.04-0.05-0.07-0.08c-0.28-0.3-0.68-0.47-1.09-0.47h-1.39c-0.3,0-0.6,0.09-0.84,0.26c-0.67-0.3-1.39-0.46-2.12-0.46 c-1.83,0-3.43,1-4.37,2.5c-0.2-0.46-0.48-0.89-0.83-1.25c-0.8-0.81-1.89-1.25-3.02-1.25h-0.01c-0.89,0.01-1.75,0.33-2.46,0.88 c-0.74-0.57-1.64-0.88-2.57-0.88H28.1c-0.29,0-0.58,0.03-0.86,0.11c-0.28,0.06-0.56,0.16-0.82,0.28c-0.21-0.12-0.45-0.18-0.7-0.18 h-1.4c-0.82,0-1.47,0.66-1.47,1.47v7.5c0,0.82,0.66,1.47,1.47,1.47h1.4c0.82,0,1.48-0.66,1.48-1.48l0,0V9.79 c0.03-0.36,0.23-0.59,0.36-0.59c0.18,0,0.38,0.18,0.38,0.47v4.57c0,0.82,0.66,1.47,1.47,1.47h1.41c0.82,0,1.47-0.66,1.47-1.47 l-0.01-4.57c0.06-0.32,0.25-0.47,0.35-0.47c0.18,0,0.38,0.18,0.38,0.47v4.57c0,0.82,0.66,1.47,1.47,1.47h1.41 c0.82,0,1.47-0.66,1.47-1.47v-0.38c0.96,1.29,2.46,2.06,4.06,2.06c0.74,0,1.46-0.16,2.12-0.47c0.24,0.17,0.54,0.26,0.84,0.26h1.39 c0.3,0,0.6-0.09,0.84-0.26v2.01c0,0.82,0.66,1.47,1.47,1.47h1.4c0.82,0,1.47-0.66,1.47-1.47v-1.77c0.48,0.15,0.99,0.23,1.49,0.22 c1.7,0,3.22-0.87,4.17-2.2v0.52c0,0.82,0.66,1.47,1.47,1.47h1.4c0.3,0,0.6-0.09,0.84-0.26c0.66,0.31,1.39,0.47,2.12,0.47 c1.92,0,3.6-1.1,4.49-2.73c1.54,2.65,4.95,3.53,7.58,1.98c0.18-0.11,0.36-0.22,0.53-0.36c0.22,0.55,0.76,0.91,1.35,0.9H78 c0.56,0,1.08-0.29,1.37-0.78l0.37-0.61l0.37,0.61c0.29,0.48,0.81,0.78,1.38,0.78h1.6c0.81,0,1.46-0.66,1.45-1.46 C84.49,14.02,84.44,13.8,84.34,13.59L84.34,13.59z M35.86,14.47h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68 c0-0.98-0.74-1.71-1.62-1.71c-0.8,0-1.46,0.7-1.59,1.62l0.01,4.66c0,0.13-0.1,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23 V9.68c0-0.98-0.74-1.71-1.62-1.71c-0.85,0-1.54,0.79-1.6,1.8v4.48c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23 V6.74c0.01-0.13,0.11-0.22,0.23-0.22h1.4c0.13,0,0.22,0.11,0.23,0.22V7.4c0.5-0.68,1.3-1.09,2.16-1.1h0.03 c1.09,0,2.09,0.6,2.6,1.55c0.45-0.95,1.4-1.55,2.44-1.56c1.62,0,2.93,1.25,2.9,2.78l0.01,5.16C36.09,14.36,35.98,14.46,35.86,14.47 L35.86,14.47z M45.97,14.24c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V13.5c-0.7,0.76-1.69,1.18-2.72,1.18 c-2.17,0-3.94-1.87-3.94-4.19s1.77-4.19,3.94-4.19c1.03,0,2.02,0.43,2.73,1.18V6.74c0-0.13,0.1-0.23,0.23-0.23h1.4 c0.12-0.01,0.22,0.08,0.23,0.21c0,0.01,0,0.01,0,0.02v7.51h-0.01V14.24z M52.41,14.67c-1.03,0-2.02-0.43-2.73-1.18v3.97 c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.1-0.23-0.23V6.75c0-0.13,0.1-0.22,0.23-0.22h1.4c0.13,0,0.23,0.11,0.23,0.23v0.73 c0.71-0.76,1.7-1.18,2.73-1.18c2.17,0,3.94,1.86,3.94,4.18S54.58,14.67,52.41,14.67z M66.24,11.39c-0.39,1.87-1.96,3.29-3.84,3.29 c-1.03,0-2.02-0.43-2.73-1.18v0.73c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V3.51c0-0.13,0.1-0.23,0.23-0.23 h1.4c0.13,0,0.23,0.11,0.23,0.23v3.97c0.71-0.75,1.7-1.18,2.73-1.17c1.88,0,3.45,1.4,3.84,3.28C66.37,10.19,66.37,10.8,66.24,11.39 L66.24,11.39L66.24,11.39z M71.67,14.68c-2,0.01-3.73-1.35-4.17-3.3c-0.13-0.59-0.13-1.19,0-1.77c0.44-1.94,2.17-3.31,4.17-3.3 c2.36,0,4.26,1.87,4.26,4.19S74.03,14.68,71.67,14.68L71.67,14.68z M83.04,14.47h-1.61c-0.13,0-0.24-0.06-0.3-0.17l-1.44-2.39 l-1.44,2.39c-0.06,0.11-0.18,0.17-0.3,0.17h-1.61c-0.04,0-0.08-0.01-0.12-0.03c-0.09-0.06-0.13-0.19-0.06-0.28l0,0l2.43-3.68 L76.2,6.84c-0.02-0.03-0.03-0.07-0.03-0.12c0-0.12,0.09-0.21,0.21-0.21h1.61c0.13,0,0.24,0.06,0.3,0.17l1.41,2.36l1.41-2.36 c0.06-0.11,0.18-0.17,0.3-0.17h1.61c0.04,0,0.08,0.01,0.12,0.03c0.09,0.06,0.13,0.19,0.06,0.28l0,0l-2.38,3.64l2.43,3.67 c0.02,0.03,0.03,0.07,0.03,0.12C83.25,14.38,83.16,14.47,83.04,14.47L83.04,14.47L83.04,14.47z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M10.5,1.24c-5.11,0-9.25,4.15-9.25,9.25s4.15,9.25,9.25,9.25s9.25-4.15,9.25-9.25 C19.75,5.38,15.61,1.24,10.5,1.24z M14.89,12.77c-1.93,1.93-4.78,2.31-6.7,2.31c-0.7,0-1.41-0.05-2.1-0.16c0,0-1.02-5.64,2.14-8.81 c0.83-0.83,1.95-1.28,3.13-1.28c1.27,0,2.49,0.51,3.39,1.42C16.59,8.09,16.64,11,14.89,12.77z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M10.5-0.01C4.7-0.01,0,4.7,0,10.49s4.7,10.5,10.5,10.5S21,16.29,21,10.49C20.99,4.7,16.3-0.01,10.5-0.01z M10.5,19.74c-5.11,0-9.25-4.15-9.25-9.25s4.14-9.26,9.25-9.26s9.25,4.15,9.25,9.25C19.75,15.61,15.61,19.74,10.5,19.74z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M14.74,6.25C12.9,4.41,9.98,4.35,8.23,6.1c-3.16,3.17-2.14,8.81-2.14,8.81s5.64,1.02,8.81-2.14 C16.64,11,16.59,8.09,14.74,6.25z M12.47,10.34l-0.91,1.87l-0.9-1.87L8.8,9.43l1.86-0.9l0.9-1.87l0.91,1.87l1.86,0.9L12.47,10.34z'/%3E %3Cpolygon class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' points='14.33,9.43 12.47,10.34 11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 '/%3E%3C/g%3E%3C/svg%3E\");\n}\n\na.mapboxgl-ctrl-logo.mapboxgl-compact {\n width: 21px;\n height: 21px;\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 21 21' style='enable-background:new 0 0 21 21;' xml:space='preserve'%3E%3Cg transform='translate(0,0.01)'%3E%3Cpath d='m 10.5,1.24 c -5.11,0 -9.25,4.15 -9.25,9.25 0,5.1 4.15,9.25 9.25,9.25 5.1,0 9.25,-4.15 9.25,-9.25 0,-5.11 -4.14,-9.25 -9.25,-9.25 z m 4.39,11.53 c -1.93,1.93 -4.78,2.31 -6.7,2.31 -0.7,0 -1.41,-0.05 -2.1,-0.16 0,0 -1.02,-5.64 2.14,-8.81 0.83,-0.83 1.95,-1.28 3.13,-1.28 1.27,0 2.49,0.51 3.39,1.42 1.84,1.84 1.89,4.75 0.14,6.52 z' style='opacity:0.9;fill:%23ffffff;enable-background:new' class='st0'/%3E%3Cpath d='M 10.5,-0.01 C 4.7,-0.01 0,4.7 0,10.49 c 0,5.79 4.7,10.5 10.5,10.5 5.8,0 10.5,-4.7 10.5,-10.5 C 20.99,4.7 16.3,-0.01 10.5,-0.01 Z m 0,19.75 c -5.11,0 -9.25,-4.15 -9.25,-9.25 0,-5.1 4.14,-9.26 9.25,-9.26 5.11,0 9.25,4.15 9.25,9.25 0,5.13 -4.14,9.26 -9.25,9.26 z' style='opacity:0.35;enable-background:new' class='st1'/%3E%3Cpath d='M 14.74,6.25 C 12.9,4.41 9.98,4.35 8.23,6.1 5.07,9.27 6.09,14.91 6.09,14.91 c 0,0 5.64,1.02 8.81,-2.14 C 16.64,11 16.59,8.09 14.74,6.25 Z m -2.27,4.09 -0.91,1.87 -0.9,-1.87 -1.86,-0.91 1.86,-0.9 0.9,-1.87 0.91,1.87 1.86,0.9 z' style='opacity:0.35;enable-background:new' class='st1'/%3E%3Cpolygon points='11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 14.33,9.43 12.47,10.34 ' style='opacity:0.9;fill:%23ffffff;enable-background:new' class='st0'/%3E%3C/g%3E%3C/svg%3E\");\n}\n\n.mapboxgl-ctrl.mapboxgl-ctrl-attrib {\n padding: 0 5px;\n background-color: rgba(255, 255, 255, 0.5);\n margin: 0;\n}\n\n@media screen {\n .mapboxgl-ctrl-attrib.mapboxgl-compact {\n padding-top: 2px;\n padding-bottom: 2px;\n margin: 0 10px 10px;\n position: relative;\n padding-right: 24px;\n background-color: #fff;\n border-radius: 3px 12px 12px 3px;\n visibility: hidden;\n }\n\n .mapboxgl-ctrl-attrib.mapboxgl-compact:hover {\n visibility: visible;\n }\n\n .mapboxgl-ctrl-attrib.mapboxgl-compact::after {\n content: '';\n cursor: pointer;\n position: absolute;\n bottom: 0;\n right: 0;\n background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath fill='%23333333' fill-rule='evenodd' d='M4,10a6,6 0 1,0 12,0a6,6 0 1,0 -12,0 M9,7a1,1 0 1,0 2,0a1,1 0 1,0 -2,0 M9,10a1,1 0 1,1 2,0l0,3a1,1 0 1,1 -2,0'/%3E %3C/svg%3E\");\n background-color: rgba(255, 255, 255, 0.5);\n width: 24px;\n height: 24px;\n box-sizing: border-box;\n visibility: visible;\n border-radius: 12px;\n }\n}\n\n.mapboxgl-ctrl-attrib a {\n color: rgba(0, 0, 0, 0.75);\n text-decoration: none;\n}\n\n.mapboxgl-ctrl-attrib a:hover {\n color: inherit;\n text-decoration: underline;\n}\n\n/* stylelint-disable-next-line selector-class-pattern */\n.mapboxgl-ctrl-attrib .mapbox-improve-map {\n font-weight: bold;\n margin-left: 2px;\n}\n\n.mapboxgl-attrib-empty {\n display: none;\n}\n\n.mapboxgl-ctrl-scale {\n background-color: rgba(255, 255, 255, 0.75);\n font-size: 10px;\n border-width: medium 2px 2px;\n border-style: none solid solid;\n border-color: #333;\n padding: 0 5px;\n color: #333;\n box-sizing: border-box;\n}\n\n.mapboxgl-popup {\n position: absolute;\n top: 0;\n left: 0;\n display: -webkit-flex;\n display: flex;\n will-change: transform;\n pointer-events: none;\n}\n\n.mapboxgl-popup-anchor-top,\n.mapboxgl-popup-anchor-top-left,\n.mapboxgl-popup-anchor-top-right {\n -webkit-flex-direction: column;\n flex-direction: column;\n}\n\n.mapboxgl-popup-anchor-bottom,\n.mapboxgl-popup-anchor-bottom-left,\n.mapboxgl-popup-anchor-bottom-right {\n -webkit-flex-direction: column-reverse;\n flex-direction: column-reverse;\n}\n\n.mapboxgl-popup-anchor-left {\n -webkit-flex-direction: row;\n flex-direction: row;\n}\n\n.mapboxgl-popup-anchor-right {\n -webkit-flex-direction: row-reverse;\n flex-direction: row-reverse;\n}\n\n.mapboxgl-popup-tip {\n width: 0;\n height: 0;\n border: 10px solid transparent;\n z-index: 1;\n}\n\n.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {\n -webkit-align-self: center;\n align-self: center;\n border-top: none;\n border-bottom-color: #fff;\n}\n\n.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {\n -webkit-align-self: flex-start;\n align-self: flex-start;\n border-top: none;\n border-left: none;\n border-bottom-color: #fff;\n}\n\n.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {\n -webkit-align-self: flex-end;\n align-self: flex-end;\n border-top: none;\n border-right: none;\n border-bottom-color: #fff;\n}\n\n.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {\n -webkit-align-self: center;\n align-self: center;\n border-bottom: none;\n border-top-color: #fff;\n}\n\n.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {\n -webkit-align-self: flex-start;\n align-self: flex-start;\n border-bottom: none;\n border-left: none;\n border-top-color: #fff;\n}\n\n.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {\n -webkit-align-self: flex-end;\n align-self: flex-end;\n border-bottom: none;\n border-right: none;\n border-top-color: #fff;\n}\n\n.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {\n -webkit-align-self: center;\n align-self: center;\n border-left: none;\n border-right-color: #fff;\n}\n\n.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {\n -webkit-align-self: center;\n align-self: center;\n border-right: none;\n border-left-color: #fff;\n}\n\n.mapboxgl-popup-close-button {\n position: absolute;\n right: 0;\n top: 0;\n border: 0;\n border-radius: 0 3px 0 0;\n cursor: pointer;\n background-color: transparent;\n}\n\n.mapboxgl-popup-close-button:hover {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.mapboxgl-popup-content {\n position: relative;\n background: #fff;\n border-radius: 3px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n padding: 10px 10px 15px;\n pointer-events: auto;\n}\n\n.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {\n border-top-left-radius: 0;\n}\n\n.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {\n border-top-right-radius: 0;\n}\n\n.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {\n border-bottom-left-radius: 0;\n}\n\n.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {\n border-bottom-right-radius: 0;\n}\n\n.mapboxgl-marker {\n position: absolute;\n top: 0;\n left: 0;\n will-change: transform;\n}\n\n.mapboxgl-user-location-dot {\n background-color: #1da1f2;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.25);\n}\n\n.mapboxgl-user-location-dot::before {\n background-color: #1da1f2;\n content: '';\n width: 15px;\n height: 15px;\n border-radius: 50%;\n position: absolute;\n -webkit-animation: mapboxgl-user-location-dot-pulse 2s infinite;\n -moz-animation: mapboxgl-user-location-dot-pulse 2s infinite;\n -ms-animation: mapboxgl-user-location-dot-pulse 2s infinite;\n animation: mapboxgl-user-location-dot-pulse 2s infinite;\n}\n\n.mapboxgl-user-location-dot::after {\n border-radius: 50%;\n border: 2px solid #fff;\n content: '';\n height: 19px;\n left: -2px;\n position: absolute;\n top: -2px;\n width: 19px;\n box-sizing: border-box;\n}\n\n@-webkit-keyframes mapboxgl-user-location-dot-pulse {\n 0% { -webkit-transform: scale(1); opacity: 1; }\n 70% { -webkit-transform: scale(3); opacity: 0; }\n 100% { -webkit-transform: scale(1); opacity: 0; }\n}\n\n@-ms-keyframes mapboxgl-user-location-dot-pulse {\n 0% { -ms-transform: scale(1); opacity: 1; }\n 70% { -ms-transform: scale(3); opacity: 0; }\n 100% { -ms-transform: scale(1); opacity: 0; }\n}\n\n@keyframes mapboxgl-user-location-dot-pulse {\n 0% { transform: scale(1); opacity: 1; }\n 70% { transform: scale(3); opacity: 0; }\n 100% { transform: scale(1); opacity: 0; }\n}\n\n.mapboxgl-user-location-dot-stale {\n background-color: #aaa;\n}\n\n.mapboxgl-user-location-dot-stale::after {\n display: none;\n}\n\n.mapboxgl-crosshair,\n.mapboxgl-crosshair .mapboxgl-interactive,\n.mapboxgl-crosshair .mapboxgl-interactive:active {\n cursor: crosshair;\n}\n\n.mapboxgl-boxzoom {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n background: #fff;\n border: 2px dotted #202020;\n opacity: 0.5;\n}\n\n@media print {\n /* stylelint-disable-next-line selector-class-pattern */\n .mapbox-improve-map {\n display: none;\n }\n}\n",""]);const a=r},323:(n,e,t)=>{t.d(e,{Z:()=>a});var o=t(645),r=t.n(o)()(!1);r.push([n.id,".mapboxgl-map .mapboxgl-canvas {\n top: 0;\n left: 0;\n}\n",""]);const a=r},645:n=>{n.exports=function(n){var e=[];return e.toString=function(){return this.map((function(e){var t=function(n,e){var t,o,r,a=n[1]||"",i=n[3];if(!i)return a;if(e&&"function"==typeof btoa){var c=(t=i,o=btoa(unescape(encodeURIComponent(JSON.stringify(t)))),r="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(o),"/*# ".concat(r," */")),l=i.sources.map((function(n){return"/*# sourceURL=".concat(i.sourceRoot||"").concat(n," */")}));return[a].concat(l).concat([c]).join("\n")}return[a].join("\n")}(e,n);return e[2]?"@media ".concat(e[2]," {").concat(t,"}"):t})).join("")},e.i=function(n,t,o){"string"==typeof n&&(n=[[null,n,""]]);var r={};if(o)for(var a=0;a{var o,r=function(){var n={};return function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(n){t=null}n[e]=t}return n[e]}}(),a=[];function i(n){for(var e=-1,t=0;t{n.exports=require("mapbox-gl")}},e={};function t(o){if(e[o])return e[o].exports;var r=e[o]={id:o,exports:{}};return n[o](r,r.exports,t),r.exports}return t.n=n=>{var e=n&&n.__esModule?()=>n.default:()=>n;return t.d(e,{a:e}),e},t.d=(n,e)=>{for(var o in e)t.o(e,o)&&!t.o(n,o)&&Object.defineProperty(n,o,{enumerable:!0,get:e[o]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=(n,e)=>Object.prototype.hasOwnProperty.call(n,e),t.r=n=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t(133)})();
--------------------------------------------------------------------------------
/esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./esdoc",
4 | "includes": ["\\.(js|jsx)$"],
5 | "excludes": ["\\.test\\.(js|jsx)$", "esdoc/.*$", "Diagnose/*"],
6 | "unexportIdentifier": true,
7 | "package": "./package.json",
8 | "plugins": [
9 | {
10 | "name": "esdoc-ecmascript-proposal-plugin",
11 | "option": {
12 | "classProperties": true,
13 | "objectRestSpread": true,
14 | "decorators": true,
15 | "doExpressions": true,
16 | "functionBind": true,
17 | "asyncGenerators": true,
18 | "exportExtensions": true,
19 | "dynamicImport": true
20 | }
21 | },
22 | { "name": "esdoc-jsx-plugin" },
23 | { "name": "esdoc-publish-html-plugin" },
24 | { "name": "esdoc-coverage-plugin" },
25 | { "name": "esdoc-plugin-require-coverage" }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
2 |
3 | You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md).
4 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://meilleursagents.github.io/react-mapbox-wrapper",
6 | "dependencies": {
7 | "@sentry/browser": "5.6.3",
8 | "@turf/circle": "^6.0.1",
9 | "mapbox-gl": "^1.1.1",
10 | "places.js": "^1.16.4",
11 | "prop-types": "15.7.2",
12 | "react": "16.8.6",
13 | "react-dom": "16.8.6",
14 | "react-mapbox-wrapper": "2.5.0",
15 | "react-scripts": "3.0.1",
16 | "react-syntax-highlighter": "11.0.2"
17 | },
18 | "scripts": {
19 | "link": "npm link react-mapbox-wrapper",
20 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start",
21 | "build": "SKIP_PREFLIGHT_CHECK=true react-scripts build",
22 | "test": "SKIP_PREFLIGHT_CHECK=true react-scripts test",
23 | "eject": "react-scripts eject",
24 | "predeploy": "npm run build",
25 | "deploy": "gh-pages -d build"
26 | },
27 | "devDependencies": {
28 | "gh-pages": "2.0.1"
29 | },
30 | "browserslist": [
31 | ">0.2%",
32 | "not dead",
33 | "not ie <= 11",
34 | "not op_mini all"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/examples/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MeilleursAgents/react-mapbox-wrapper/5bcb39b5b768c46474136b21c75428da92fbc505/examples/public/favicon-32x32.png
--------------------------------------------------------------------------------
/examples/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | react-mapbox-wrapper
10 |
11 |
12 |
13 | You need to enable JavaScript to run this app.
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-mapbox-wrapper",
3 | "name": "Mapbox GL Javascript wrapper for ReactJS",
4 | "icons": [
5 | {
6 | "src": "favicon-32x32.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/examples/src/AllInOne/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import MapboxMap, { Marker, Circle, Helpers } from 'react-mapbox-wrapper';
3 |
4 | const SENTIER_COORDINATES = { lat: 48.868526, lng: 2.3434886 };
5 | const RADIUS_COORDINATES = { lat: 48.870362, lng: 2.3400597 };
6 |
7 | export default class AllInOne extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.onMapLoad = this.onMapLoad.bind(this);
12 | }
13 |
14 | onMapLoad(map) {
15 | this.map = map;
16 | this.forceUpdate();
17 |
18 | const bounds = Helpers.newBounds();
19 | bounds.extend(Helpers.newBound({ lat: 48.872198, lng: 2.3366308 }));
20 | bounds.extend(Helpers.newBound(SENTIER_COORDINATES));
21 |
22 | this.map.jumpTo(this.map.cameraForBounds(bounds, { padding: 120 }));
23 | }
24 |
25 | render() {
26 | let markers;
27 | if (this.map) {
28 | const popupHaussmann = Happy to be here
;
29 | const popupSentier = Old home
;
30 |
31 | markers = [
32 |
41 |
42 | 🏢
43 |
44 | ,
45 |
54 |
55 | 🏠
56 |
57 | ,
58 | ,
70 | ];
71 | }
72 |
73 | return (
74 |
83 | {markers}
84 |
85 | );
86 | }
87 | }
88 |
89 | AllInOne.displayName = 'AllInOne';
90 |
--------------------------------------------------------------------------------
/examples/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | * {
8 | box-sizing: border-box;
9 | }
10 |
11 | #root {
12 | text-align: center;
13 | }
14 |
15 | .margin-left {
16 | margin-left: 10px;
17 | }
18 |
19 | .header {
20 | background-color: rgb(0, 116, 228);
21 | color: white;
22 | padding: 5px;
23 | }
24 |
25 | .ap-input-wrapper {
26 | margin: 20px auto;
27 | margin-bottom: 40px;
28 | text-align: left;
29 | color: black;
30 | width: 400px;
31 | }
32 |
33 | .disclaimer {
34 | display: block;
35 | margin-top: 5px;
36 | font-size: 12px;
37 | color: gray;
38 | }
39 |
40 | .footer {
41 | padding: 20px 10px;
42 | font-size: 12px;
43 | }
44 |
45 | .link,
46 | .link:visited,
47 | .link:focus,
48 | .link:hover {
49 | background-color: rgb(0, 116, 228);
50 | border-radius: 5px;
51 | color: white;
52 | display: inline-block;
53 | margin-top: 10px;
54 | padding: 10px;
55 | text-decoration: none;
56 | }
57 |
58 | .map-container {
59 | flex: 1 1;
60 | min-height: 400px;
61 | }
62 |
63 | .example {
64 | display: flex;
65 | justify-content: center;
66 | }
67 |
68 | .code {
69 | flex: 1 1;
70 | height: 100%;
71 | margin: 0 0 0 10px !important;
72 | min-height: 400px;
73 | }
74 |
--------------------------------------------------------------------------------
/examples/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3 | import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism';
4 | import { Diagnose } from 'react-mapbox-wrapper';
5 | import Places from 'places.js';
6 | import SimpleMap from './SimpleMap';
7 | import MapWithMarker from './MapWithMarker';
8 | import MapWithMovingMarker from './MapWithMovingMarker';
9 | import MarkerWithPopup from './MarkerWithPopup';
10 | import CustomMarkerOnOver from './CustomMarkerOnOver';
11 | import MapWithCircle from './MapWithCircle';
12 | import MapWithNavigationControlsPosition from './MapWithNavigationControlsPosition';
13 | import AllInOne from './AllInOne';
14 | import './App.css';
15 |
16 | /**
17 | * Public access token retrieved from https://github.com/mapbox/mapbox-gl-supported/blob/gh-pages/diagnostics.html.
18 | * @type {String}
19 | */
20 | global.ACCESS_TOKEN =
21 | 'pk.eyJ1IjoibHVjYXN3b2oiLCJhIjoiY2l5Nmg4cWU1MDA0ejMzcDJtNHJmZzJkcyJ9.WhcEdTYQH6sSw2pm0RSP9Q';
22 |
23 | /**
24 | * App Component.
25 | */
26 | export default class App extends Component {
27 | /**
28 | * Creates an instance of App.
29 | * @param {Object} props Component props
30 | */
31 | constructor(props) {
32 | super(props);
33 |
34 | this.state = {
35 | coordinates: {
36 | lat: 48.872198,
37 | lng: 2.3366308,
38 | },
39 | };
40 | }
41 |
42 | /**
43 | * React lifecycle.
44 | */
45 | componentDidMount() {
46 | const placesAutocomplete = Places({
47 | container: document.querySelector('#address-input'),
48 | });
49 |
50 | placesAutocomplete.on('change', e => this.setState({ coordinates: e.suggestion.latlng }));
51 |
52 | Diagnose.fullDiagnostic().then((content) => {
53 | global.console.debug(content);
54 | });
55 | }
56 |
57 | /**
58 | * React lifecycle.
59 | */
60 | render() {
61 | const { coordinates } = this.state;
62 |
63 | return (
64 |
65 |
66 | react-mapbox-wrapper
67 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
88 | Documentation
89 |
90 |
96 | Examples sources
97 |
98 |
99 |
100 |
101 |
102 | We use algolia's default api key, places may have imprecise coordinates
103 |
104 |
105 |
106 |
107 |
SimpleMap with fixed size
108 |
109 |
110 |
111 | {`import React from 'react';
112 | import MapboxMap from 'react-mapbox-wrapper';
113 |
114 | export default function SimpleMap({coordinates}) {
115 | return (
116 |
117 |
121 |
122 | );
123 | }
124 |
125 | SimpleMap.displayName = 'SimpleMap';`}
126 |
127 |
128 |
129 |
130 |
131 |
Marker
132 |
133 |
134 |
135 | {`import React, { Component } from 'react';
136 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
137 |
138 | export default class MapWithMarker extends Component {
139 | constructor(props) {
140 | super(props);
141 |
142 | this.onMapLoad = this.onMapLoad.bind(this);
143 | }
144 |
145 | onMapLoad(map) {
146 | this.map = map;
147 | this.forceUpdate();
148 | }
149 |
150 | render() {
151 | let marker;
152 | const { coordinates } = this.props;
153 |
154 | if (this.map) {
155 | marker = ;
156 | }
157 |
158 | return (
159 |
165 | {marker}
166 |
167 | );
168 | }
169 | }
170 |
171 | MapWithMarker.displayName = 'MapWithMarker';`}
172 |
173 |
174 |
175 |
176 |
177 |
Moving Marker with array-style LngLat
178 |
179 |
180 |
181 | {`import React, { Component } from 'react';
182 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
183 |
184 | export default class MapWithMovingMarker extends Component {
185 | constructor(props) {
186 | super(props);
187 |
188 | this.state = {
189 | coordinates: props.coordinates,
190 | };
191 |
192 | this.onMapLoad = this.onMapLoad.bind(this);
193 | }
194 |
195 | componentDidMount() {
196 | setInterval(() => {
197 | const { lat, lng } = this.props.coordinates;
198 |
199 | this.setState({
200 | coordinates: [lng + 0.001 * Math.random(), lat + 0.001 * Math.random()],
201 | });
202 | }, 1000);
203 | }
204 |
205 | onMapLoad(map) {
206 | this.map = map;
207 | this.forceUpdate();
208 | }
209 |
210 | render() {
211 | let marker;
212 | const { coordinates } = this.props;
213 |
214 | if (this.map) {
215 | marker = ;
216 | }
217 |
218 | return (
219 |
225 | {marker}
226 |
227 | );
228 | }
229 | }
230 |
231 | MapWithMovingMarker.displayName = 'MapWithMovingMarker';`}
232 |
233 |
234 |
235 |
236 |
237 |
Marker with Popup on Click
238 |
239 |
240 |
241 | {`import React, { Component } from 'react';
242 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
243 |
244 | export default class MarkerWithPopup extends Component {
245 | constructor(props) {
246 | super(props);
247 |
248 | this.onMapLoad = this.onMapLoad.bind(this);
249 | }
250 |
251 | onMapLoad(map) {
252 | this.map = map;
253 | this.forceUpdate();
254 | }
255 |
256 | render() {
257 | let marker;
258 | const { coordinates } = this.props;
259 |
260 | if (this.map) {
261 | const popup = Meaningful content on my Marker
;
262 |
263 | marker = (
264 |
271 | );
272 | }
273 |
274 | return (
275 |
281 | {marker}
282 |
283 | );
284 | }
285 | }
286 |
287 | MarkerWithPopup.displayName = 'MarkerWithPopup';`}
288 |
289 |
290 |
291 |
292 |
293 |
Custom Marker with Popup on Over
294 |
295 |
296 |
297 | {`import React, { Component } from 'react';
298 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
299 |
300 | export default class CustomMarker extends Component {
301 | constructor(props) {
302 | super(props);
303 |
304 | this.onMapLoad = this.onMapLoad.bind(this);
305 | }
306 |
307 | onMapLoad(map) {
308 | this.map = map;
309 | this.forceUpdate();
310 | }
311 |
312 | render() {
313 | let marker;
314 | const { coordinates } = this.props;
315 |
316 | if (this.map) {
317 | const popup = http://localhost
;
318 |
319 | marker = (
320 |
328 |
329 | 🏢
330 |
331 |
332 | );
333 | }
334 |
335 | return (
336 |
342 | {marker}
343 |
344 | );
345 | }
346 | }
347 |
348 | CustomMarker.displayName = 'CustomMarker';`}
349 |
350 |
351 |
352 |
353 |
354 |
Circle
355 |
356 |
357 |
358 | {`import React, { Component } from 'react';
359 | import MapboxMap, { Circle } from 'react-mapbox-wrapper';
360 |
361 | export default class MapWithCircle extends Component {
362 | constructor(props) {
363 | super(props);
364 |
365 | this.onMapLoad = this.onMapLoad.bind(this);
366 | }
367 |
368 | onMapLoad(map) {
369 | this.map = map;
370 | this.forceUpdate();
371 | }
372 |
373 | render() {
374 | let circle;
375 | const { coordinates } = this.props;
376 |
377 | if(this.map) {
378 | circle =
390 | }
391 |
392 | return (
393 |
399 | {circle}
400 |
401 | );
402 | }
403 | }
404 |
405 | MapWithCircle.displayName = 'MapWithCircle';`}
406 |
407 |
408 |
409 |
410 |
411 |
Map navigation controls with custom position
412 |
413 |
414 |
415 | {`import React from 'react';
416 | import PropTypes from 'prop-types';
417 | import MapboxMap from 'react-mapbox-wrapper';
418 |
419 | /**
420 | * MapControlPosition Functional Component.
421 | */
422 | export default function MapWithNavigationControlsPosition({coordinates}) {
423 | return (
424 |
434 | );
435 | };
436 |
437 | MapWithNavigationControlsPosition.displayName = 'MapWithNavigationControlsPosition';`}
438 |
439 |
440 |
441 |
442 |
443 |
Radius, Marker, Popup and fitBounds
444 |
445 |
446 |
447 | {`import React, { Component } from 'react';
448 | import MapboxMap, { Marker, Circle, Helpers } from 'react-mapbox-wrapper';
449 |
450 | const SENTIER_COORDINATES = { lat: 48.868526, lng: 2.3434886 };
451 | const RADIUS_COORDINATES = { lat: 48.870362, lng: 2.3400597 };
452 |
453 | export default class AllInOne extends Component {
454 | constructor(props) {
455 | super(props);
456 |
457 | this.onMapLoad = this.onMapLoad.bind(this);
458 | }
459 |
460 | onMapLoad(map, coordinates) {
461 | this.map = map;
462 | this.forceUpdate();
463 |
464 | const bounds = Helpers.newBounds();
465 | bounds.extend(Helpers.newBound({lat: 48.872198, lng: 2.3366308}));
466 | bounds.extend(Helpers.newBound(SENTIER_COORDINATES));
467 |
468 | this.map.jumpTo(this.map.cameraForBounds(bounds, { padding: 120 }));
469 | }
470 |
471 | render() {
472 | let markers;
473 | if (this.map) {
474 | const popupHaussmann = Happy to be here
;
475 | const popupSentier = Old home
;
476 |
477 | markers = [
478 |
487 |
488 | 🏢
489 |
490 | ,
491 |
500 |
501 | 🏠
502 |
503 | ,
504 | ,
516 | ];
517 | }
518 |
519 | return (
520 |
529 | {markers}
530 |
531 | );
532 | }
533 | }
534 |
535 | AllInOne.displayName = 'AllInOne';`}
536 |
537 |
538 |
539 |
540 |
554 |
555 | );
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/examples/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/examples/src/CustomMarkerOnOver/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
4 |
5 | export default class CustomMarker extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.onMapLoad = this.onMapLoad.bind(this);
10 | }
11 |
12 | onMapLoad(map) {
13 | this.map = map;
14 | this.forceUpdate();
15 | }
16 |
17 | render() {
18 | let marker;
19 | const { coordinates } = this.props;
20 |
21 | if (this.map) {
22 | const popup = http://localhost
;
23 |
24 | marker = (
25 |
33 |
34 | 🏢
35 |
36 |
37 | );
38 | }
39 |
40 | return (
41 |
47 | {marker}
48 |
49 | );
50 | }
51 | }
52 |
53 | CustomMarker.displayName = 'CustomMarker';
54 |
55 | CustomMarker.propTypes = {
56 | coordinates: PropTypes.oneOfType([
57 | PropTypes.shape({
58 | lat: PropTypes.number.isRequired,
59 | lng: PropTypes.number.isRequired,
60 | }),
61 | PropTypes.shape({
62 | lat: PropTypes.number.isRequired,
63 | lon: PropTypes.number.isRequired,
64 | }),
65 | PropTypes.arrayOf(PropTypes.number.isRequired),
66 | ]).isRequired,
67 | };
68 |
--------------------------------------------------------------------------------
/examples/src/MapWithCircle/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap, { Circle } from 'react-mapbox-wrapper';
4 |
5 | export default class MapWithCircle extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.onMapLoad = this.onMapLoad.bind(this);
10 | }
11 |
12 | onMapLoad(map) {
13 | this.map = map;
14 | this.forceUpdate();
15 | }
16 |
17 | render() {
18 | let circle;
19 | const { coordinates } = this.props;
20 |
21 | if (this.map) {
22 | circle = (
23 |
35 | );
36 | }
37 |
38 | return (
39 |
45 | {circle}
46 |
47 | );
48 | }
49 | }
50 |
51 | MapWithCircle.displayName = 'MapWithCircle';
52 |
53 | MapWithCircle.propTypes = {
54 | coordinates: PropTypes.oneOfType([
55 | PropTypes.shape({
56 | lat: PropTypes.number.isRequired,
57 | lng: PropTypes.number.isRequired,
58 | }),
59 | PropTypes.shape({
60 | lat: PropTypes.number.isRequired,
61 | lon: PropTypes.number.isRequired,
62 | }),
63 | PropTypes.arrayOf(PropTypes.number.isRequired),
64 | ]).isRequired,
65 | };
66 |
--------------------------------------------------------------------------------
/examples/src/MapWithMarker/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
4 |
5 | export default class MapWithMarker extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.onMapLoad = this.onMapLoad.bind(this);
10 | }
11 |
12 | onMapLoad(map) {
13 | this.map = map;
14 | this.forceUpdate();
15 | }
16 |
17 | render() {
18 | let marker;
19 | const { coordinates } = this.props;
20 |
21 | if (this.map) {
22 | marker = ;
23 | }
24 |
25 | return (
26 |
32 | {marker}
33 |
34 | );
35 | }
36 | }
37 |
38 | MapWithMarker.displayName = 'MapWithMarker';
39 |
40 | MapWithMarker.propTypes = {
41 | coordinates: PropTypes.oneOfType([
42 | PropTypes.shape({
43 | lat: PropTypes.number.isRequired,
44 | lng: PropTypes.number.isRequired,
45 | }),
46 | PropTypes.shape({
47 | lat: PropTypes.number.isRequired,
48 | lon: PropTypes.number.isRequired,
49 | }),
50 | PropTypes.arrayOf(PropTypes.number.isRequired),
51 | ]).isRequired,
52 | };
53 |
--------------------------------------------------------------------------------
/examples/src/MapWithMovingMarker/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
4 |
5 | export default class MapWithMovingMarker extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | coordinates: props.coordinates,
11 | };
12 |
13 | this.onMapLoad = this.onMapLoad.bind(this);
14 | }
15 |
16 | /**
17 | * React lifecycle.
18 | */
19 | componentDidMount() {
20 | setInterval(() => {
21 | const { lat, lng } = this.props.coordinates;
22 |
23 | this.setState({
24 | coordinates: [lng + 0.001 * Math.random(), lat + 0.001 * Math.random()],
25 | });
26 | }, 1000);
27 | }
28 |
29 | onMapLoad(map) {
30 | this.map = map;
31 | this.forceUpdate();
32 | }
33 |
34 | render() {
35 | let marker;
36 | const { coordinates } = this.props;
37 |
38 | if (this.map) {
39 | marker = ;
40 | }
41 |
42 | return (
43 |
49 | {marker}
50 |
51 | );
52 | }
53 | }
54 |
55 | MapWithMovingMarker.displayName = 'MapWithMovingMarker';
56 |
57 | MapWithMovingMarker.propTypes = {
58 | coordinates: PropTypes.oneOfType([
59 | PropTypes.shape({
60 | lat: PropTypes.number.isRequired,
61 | lng: PropTypes.number.isRequired,
62 | }),
63 | PropTypes.shape({
64 | lat: PropTypes.number.isRequired,
65 | lon: PropTypes.number.isRequired,
66 | }),
67 | PropTypes.arrayOf(PropTypes.number.isRequired),
68 | ]).isRequired,
69 | };
70 |
--------------------------------------------------------------------------------
/examples/src/MapWithNavigationControlsPosition/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap from 'react-mapbox-wrapper';
4 |
5 | /**
6 | * MapControlPosition Functional Component.
7 | */
8 | export default function MapWithNavigationControlsPosition({ coordinates }) {
9 | return (
10 |
20 | );
21 | }
22 |
23 | MapWithNavigationControlsPosition.displayName = 'MapWithNavigationControlsPosition';
24 |
25 | MapWithNavigationControlsPosition.propTypes = {
26 | coordinates: PropTypes.oneOfType([
27 | PropTypes.shape({
28 | lat: PropTypes.number.isRequired,
29 | lng: PropTypes.number.isRequired,
30 | }),
31 | PropTypes.shape({
32 | lat: PropTypes.number.isRequired,
33 | lon: PropTypes.number.isRequired,
34 | }),
35 | PropTypes.arrayOf(PropTypes.number.isRequired),
36 | ]).isRequired,
37 | };
38 |
--------------------------------------------------------------------------------
/examples/src/MarkerWithPopup/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap, { Marker } from 'react-mapbox-wrapper';
4 |
5 | export default class MarkerWithPopup extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.onMapLoad = this.onMapLoad.bind(this);
10 | }
11 |
12 | onMapLoad(map) {
13 | this.map = map;
14 | this.forceUpdate();
15 | }
16 |
17 | render() {
18 | let marker;
19 | const { coordinates } = this.props;
20 |
21 | if (this.map) {
22 | const popup = Meaningful content on my Marker
;
23 |
24 | marker = (
25 |
32 | );
33 | }
34 |
35 | return (
36 |
42 | {marker}
43 |
44 | );
45 | }
46 | }
47 |
48 | MarkerWithPopup.displayName = 'MarkerWithPopup';
49 |
50 | MarkerWithPopup.propTypes = {
51 | coordinates: PropTypes.oneOfType([
52 | PropTypes.shape({
53 | lat: PropTypes.number.isRequired,
54 | lng: PropTypes.number.isRequired,
55 | }),
56 | PropTypes.shape({
57 | lat: PropTypes.number.isRequired,
58 | lon: PropTypes.number.isRequired,
59 | }),
60 | PropTypes.arrayOf(PropTypes.number.isRequired),
61 | ]).isRequired,
62 | };
63 |
--------------------------------------------------------------------------------
/examples/src/SimpleMap/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import MapboxMap from 'react-mapbox-wrapper';
4 |
5 | /**
6 | * SimpleMap Functional Component.
7 | */
8 | export default function SimpleMap({ coordinates }) {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | SimpleMap.displayName = 'SimpleMap';
17 |
18 | SimpleMap.propTypes = {
19 | coordinates: PropTypes.oneOfType([
20 | PropTypes.shape({
21 | lat: PropTypes.number.isRequired,
22 | lng: PropTypes.number.isRequired,
23 | }),
24 | PropTypes.shape({
25 | lat: PropTypes.number.isRequired,
26 | lon: PropTypes.number.isRequired,
27 | }),
28 | PropTypes.arrayOf(PropTypes.number.isRequired),
29 | ]).isRequired,
30 | };
31 |
--------------------------------------------------------------------------------
/examples/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | * {
8 | box-sizing: border-box;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import * as Sentry from '@sentry/browser';
4 | import App from './App';
5 |
6 | if (process.env.NODE_ENV === 'production') {
7 | Sentry.init({dsn: "https://250287b3c5ac41809f8823a96707acca@sentry.io/1549967"});
8 | }
9 |
10 | ReactDOM.render( , document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mapbox-wrapper",
3 | "version": "2.6.1",
4 | "description": "Mapbox GL Javascript wrapper for ReactJS",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/MeilleursAgents/react-mapbox-wrapper.git"
8 | },
9 | "main": "build/index.js",
10 | "scripts": {
11 | "peers": "npm-install-peers",
12 | "format:file": "prettier-eslint --write --",
13 | "format": "npm run format:file \"$(pwd)/src/**/**.js\" \"$(pwd)/src/**/**.jsx\"",
14 | "lint:file": "eslint --fix --ext .jsx,.js --",
15 | "lint": "npm run lint:file src",
16 | "test": "nyc --all mocha \"./src/**/*.test.js\" \"./src/**/*.test.jsx\"",
17 | "test:file": "mocha",
18 | "doc": "esdoc -c esdoc.json",
19 | "start": "webpack --watch",
20 | "build": "webpack",
21 | "ci": "npm run format && npm run lint && npm run test && npm run doc && npm run build && codecov"
22 | },
23 | "keywords": [
24 | "react",
25 | "component",
26 | "library",
27 | "mapboxgl-js",
28 | "webpack"
29 | ],
30 | "author": "MeilleursAgents",
31 | "license": "MIT",
32 | "homepage": "http://meilleursagents.github.io/react-mapbox-wrapper",
33 | "engines": {
34 | "node": ">=8.0.0"
35 | },
36 | "peerDependencies": {
37 | "@turf/circle": "^6.0.0",
38 | "mapbox-gl": "^0.46.0",
39 | "prop-types": "^15.6.0",
40 | "react": "^16.0.0",
41 | "react-dom": "^16.0.0"
42 | },
43 | "devDependencies": {
44 | "@babel/core": "7.12.10",
45 | "@babel/plugin-proposal-class-properties": "7.12.1",
46 | "@babel/preset-env": "7.12.11",
47 | "@babel/preset-react": "7.12.10",
48 | "@babel/register": "7.12.10",
49 | "babel-cli": "6.26.0",
50 | "babel-eslint": "10.1.0",
51 | "babel-loader": "8.1.0",
52 | "babel-plugin-module-resolver": "4.0.0",
53 | "babel-template": "6.26.0",
54 | "babel-types": "6.26.0",
55 | "chai": "4.2.0",
56 | "codecov": "3.8.0",
57 | "css-loader": "5.0.1",
58 | "enzyme": "3.11.0",
59 | "enzyme-adapter-react-16": "1.15.5",
60 | "esdoc": "1.1.0",
61 | "esdoc-coverage-plugin": "1.1.0",
62 | "esdoc-ecmascript-proposal-plugin": "1.0.0",
63 | "esdoc-es7-plugin": "0.0.3",
64 | "esdoc-jsx-plugin": "1.0.0",
65 | "esdoc-plugin-require-coverage": "0.1.2",
66 | "esdoc-publish-html-plugin": "1.1.2",
67 | "esdoc-standard-plugin": "1.0.0",
68 | "eslint": "7.17.0",
69 | "eslint-config-airbnb": "18.2.0",
70 | "eslint-config-prettier": "7.1.0",
71 | "eslint-import-resolver-webpack": "0.13.0",
72 | "eslint-loader": "4.0.2",
73 | "eslint-plugin-babel": "5.3.1",
74 | "eslint-plugin-import": "2.22.1",
75 | "eslint-plugin-jsx-a11y": "6.3.1",
76 | "eslint-plugin-react": "7.21.4",
77 | "ignore-styles": "5.0.1",
78 | "jsdom": "16.4.0",
79 | "mocha": "8.2.1",
80 | "npm-install-peers": "1.2.1",
81 | "nyc": "15.1.0",
82 | "prettier-eslint-cli": "5.0.0",
83 | "sinon": "9.2.0",
84 | "sinon-chai": "3.5.0",
85 | "style-loader": "2.0.0",
86 | "webpack": "5.1.3",
87 | "webpack-cli": "4.0.0",
88 | "webpack-node-externals": "2.5.2"
89 | },
90 | "bugs": {
91 | "url": "https://github.com/MeilleursAgents/react-mapbox-wrapper/issues"
92 | },
93 | "directories": {
94 | "example": "examples"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MeilleursAgents/react-mapbox-wrapper/5bcb39b5b768c46474136b21c75428da92fbc505/sample.png
--------------------------------------------------------------------------------
/src/Circle/index.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { deepEqual } from 'Utils';
4 | import {
5 | coordinatesAreEqual,
6 | drawGeoJSON,
7 | removeGeoJSON,
8 | getCircleData,
9 | UNITS,
10 | LngLatLike,
11 | } from 'Helpers';
12 |
13 | /**
14 | * Update radius GeoJSON layer.
15 | * @param {Object} map MapboxMap
16 | * @param {String} id Identifier
17 | * @param {Object} coordinates Coordinates
18 | * @param {Number} radius Radius of circle in meters
19 | * @param {Object} paint Paint options
20 | * @param {Function} onClick onClick callback
21 | */
22 | function updateRadiusLayer(map, id, coordinates, radius, unit, paint, onClick) {
23 | if (!map) {
24 | return;
25 | }
26 |
27 | if (!radius) {
28 | removeGeoJSON(map, id);
29 | } else {
30 | drawGeoJSON(map, id, getCircleData(coordinates, radius, unit), paint, onClick);
31 | }
32 | }
33 |
34 | /**
35 | * Circle Component.
36 | */
37 | export default class Circle extends Component {
38 | /**
39 | * React lifecycle.
40 | */
41 | componentDidMount() {
42 | const { map, id, coordinates, radius, unit, paint, onClick } = this.props;
43 |
44 | updateRadiusLayer(map, id, coordinates, radius, unit, paint, onClick);
45 | }
46 |
47 | /**
48 | * React lifecycle.
49 | * @param {Object} prevProps Previous props
50 | */
51 | componentDidUpdate(prevProps) {
52 | const { id, map, coordinates, radius, unit, paint, onClick } = this.props;
53 | const { coordinates: prevCoord, radius: prevRadius, paint: prevPaint } = prevProps;
54 |
55 | if (
56 | !coordinatesAreEqual(coordinates, prevCoord) ||
57 | radius !== prevRadius ||
58 | !deepEqual(paint, prevPaint)
59 | ) {
60 | updateRadiusLayer(map, id, coordinates, radius, unit, paint, onClick);
61 | }
62 | }
63 |
64 | /**
65 | * React lifecycle.
66 | */
67 | componentWillUnmount() {
68 | const { id, map } = this.props;
69 |
70 | removeGeoJSON(map, id);
71 | }
72 |
73 | /**
74 | * React lifecycle.
75 | */
76 | render() {
77 | return null;
78 | }
79 | }
80 |
81 | Circle.propTypes = {
82 | coordinates: LngLatLike.isRequired,
83 | id: PropTypes.string.isRequired,
84 | map: PropTypes.shape({}).isRequired,
85 | onClick: PropTypes.func,
86 | paint: PropTypes.shape({}),
87 | radius: PropTypes.number.isRequired,
88 | unit: PropTypes.oneOf(UNITS),
89 | };
90 |
91 | Circle.defaultProps = {
92 | onClick: undefined,
93 | paint: {},
94 | unit: UNITS[0],
95 | };
96 |
--------------------------------------------------------------------------------
/src/Circle/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import sinon from 'sinon';
4 | import { getLayerId } from 'Helpers';
5 | import Circle from './index';
6 |
7 | describe(' ', () => {
8 | function defaultProps() {
9 | return {
10 | id: '8000',
11 | radius: 1000,
12 | coordinates: {
13 | lat: 20,
14 | lng: 20,
15 | },
16 | map: {
17 | getSource: sinon.spy(),
18 | addSource: sinon.spy(),
19 | removeSource: sinon.spy(),
20 | getLayer: sinon.spy(),
21 | addLayer: sinon.spy(),
22 | removeLayer: sinon.spy(),
23 | setPaintProperty: sinon.spy(),
24 | on: sinon.spy(),
25 | },
26 | paint: {
27 | 'fill-color': '#001122',
28 | },
29 | };
30 | }
31 |
32 | it('should add layer if not present', () => {
33 | const props = defaultProps();
34 | mount( );
35 |
36 | expect(props.map.addSource.called).to.equal(true);
37 | expect(props.map.addLayer.called).to.equal(true);
38 | });
39 |
40 | it('should do nothing if nothing change', () => {
41 | const props = defaultProps();
42 | const wrapper = mount( );
43 |
44 | props.map.getSource.resetHistory();
45 | wrapper.setProps({});
46 |
47 | expect(props.map.getSource.called).to.equal(false);
48 | });
49 |
50 | it('should update layer on paint change', () => {
51 | const props = defaultProps();
52 | const wrapper = mount( );
53 |
54 | const setData = sinon.spy();
55 | wrapper.setProps({
56 | ...props,
57 | paint: {
58 | 'fill-color': '#FFFFFF',
59 | },
60 | map: {
61 | ...props.map,
62 | getSource: sinon.fake.returns({ setData }),
63 | },
64 | });
65 |
66 | expect(props.map.setPaintProperty.called).to.equal(true);
67 | });
68 |
69 | it('should update layer on radius change', () => {
70 | const props = defaultProps();
71 | const wrapper = mount( );
72 |
73 | const setData = sinon.spy();
74 | wrapper.setProps({
75 | ...props,
76 | radius: 8000,
77 | map: {
78 | ...props.map,
79 | getSource: sinon.fake.returns({ setData }),
80 | },
81 | });
82 |
83 | expect(props.map.setPaintProperty.called).to.equal(true);
84 | });
85 |
86 | it('should update layer on lat coordinates change', () => {
87 | const props = defaultProps();
88 | const wrapper = mount( );
89 |
90 | const setData = sinon.spy();
91 | wrapper.setProps({
92 | ...props,
93 | coordinates: {
94 | ...props.coordinates,
95 | lat: 10,
96 | },
97 | map: {
98 | ...props.map,
99 | getSource: sinon.fake.returns({ setData }),
100 | },
101 | });
102 |
103 | expect(setData.called).to.equal(true);
104 | });
105 |
106 | it('should update layer on lng coordinates change', () => {
107 | const props = defaultProps();
108 | const wrapper = mount( );
109 |
110 | const setData = sinon.spy();
111 | wrapper.setProps({
112 | ...props,
113 | coordinates: {
114 | ...props.coordinates,
115 | lng: 10,
116 | },
117 | map: {
118 | ...props.map,
119 | getSource: sinon.fake.returns({ setData }),
120 | },
121 | });
122 |
123 | expect(setData.called).to.equal(true);
124 | });
125 |
126 | it('should remove source and layer if radius is zero', () => {
127 | const props = defaultProps();
128 | props.map.getLayer = sinon.fake.returns({});
129 | props.map.getSource = sinon.fake.returns({
130 | setData: () => null,
131 | });
132 |
133 | const wrapper = mount( );
134 | wrapper.setProps({ radius: 0 });
135 |
136 | expect(props.map.removeLayer.called).to.equal(true);
137 | expect(props.map.removeSource.called).to.equal(true);
138 | });
139 |
140 | it('should remove source and layer on unmount', () => {
141 | const props = defaultProps();
142 | props.map.getSource = sinon.fake.returns({
143 | setData: () => null,
144 | });
145 | props.map.getLayer = sinon.fake.returns(true);
146 |
147 | const wrapper = mount( );
148 |
149 | wrapper.unmount();
150 |
151 | expect(props.map.removeLayer.called).to.equal(true);
152 | expect(props.map.removeSource.called).to.equal(true);
153 | });
154 |
155 | it('should call given onClick', () => {
156 | const onClick = sinon.spy();
157 | const props = defaultProps();
158 | mount( );
159 |
160 | expect(props.map.on.calledWith('click', getLayerId(props.id), onClick)).to.equal(true);
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/Diagnose/index.js:
--------------------------------------------------------------------------------
1 | import mapboxgl from 'Lib';
2 | import { isBrowser } from 'Utils';
3 |
4 | // inspired by https://github.com/mapbox/mapbox-gl-supported/blob/gh-pages/diagnostics.html
5 |
6 | function isArraySupported() {
7 | return (
8 | Array.prototype &&
9 | Array.prototype.every &&
10 | Array.prototype.filter &&
11 | Array.prototype.forEach &&
12 | Array.prototype.indexOf &&
13 | Array.prototype.lastIndexOf &&
14 | Array.prototype.map &&
15 | Array.prototype.some &&
16 | Array.prototype.reduce &&
17 | Array.prototype.reduceRight &&
18 | Array.isArray
19 | );
20 | }
21 |
22 | function isFunctionSupported() {
23 | return Function.prototype && Function.prototype.bind;
24 | }
25 |
26 | function isObjectSupported() {
27 | return (
28 | Object.keys &&
29 | Object.create &&
30 | Object.getPrototypeOf &&
31 | Object.getOwnPropertyNames &&
32 | Object.isSealed &&
33 | Object.isFrozen &&
34 | Object.isExtensible &&
35 | Object.getOwnPropertyDescriptor &&
36 | Object.defineProperty &&
37 | Object.defineProperties &&
38 | Object.seal &&
39 | Object.freeze &&
40 | Object.preventExtensions
41 | );
42 | }
43 |
44 | function isJSONSupported() {
45 | return 'JSON' in window && 'parse' in JSON && 'stringify' in JSON;
46 | }
47 |
48 | function isWorkerSupported() {
49 | if (!('Worker' in window && 'Blob' in window && 'URL' in window)) {
50 | return false;
51 | }
52 |
53 | const blob = new Blob([''], { type: 'text/javascript' });
54 | const workerURL = URL.createObjectURL(blob);
55 | let supported;
56 | let worker;
57 |
58 | try {
59 | worker = new Worker(workerURL);
60 | supported = true;
61 | } catch (e) {
62 | supported = false;
63 | }
64 |
65 | if (worker) {
66 | worker.terminate();
67 | }
68 | URL.revokeObjectURL(workerURL);
69 |
70 | return supported;
71 | }
72 |
73 | // IE11 only supports `Uint8ClampedArray` as of version
74 | // [KB2929437](https://support.microsoft.com/en-us/kb/2929437)
75 | function isUint8ClampedArraySupported() {
76 | return 'Uint8ClampedArray' in window;
77 | }
78 |
79 | // https://github.com/mapbox/mapbox-gl-supported/issues/19
80 | function isArrayBufferSupported() {
81 | return ArrayBuffer.isView;
82 | }
83 |
84 | const webGLContextAttributes = {
85 | antialias: false,
86 | alpha: true,
87 | stencil: true,
88 | depth: true,
89 | };
90 |
91 | function isWebGLSupported(failIfMajorPerformanceCaveat) {
92 | const canvas = document.createElement('canvas');
93 | const attributes = Object.create(webGLContextAttributes);
94 | attributes.failIfMajorPerformanceCaveat = failIfMajorPerformanceCaveat;
95 |
96 | if (canvas.probablySupportsContext) {
97 | return (
98 | canvas.probablySupportsContext('webgl', attributes) ||
99 | canvas.probablySupportsContext('experimental-webgl', attributes)
100 | );
101 | }
102 |
103 | if (canvas.supportsContext) {
104 | return (
105 | canvas.supportsContext('webgl', attributes) ||
106 | canvas.supportsContext('experimental-webgl', attributes)
107 | );
108 | }
109 |
110 | return (
111 | canvas.getContext('webgl', attributes) ||
112 | canvas.getContext('experimental-webgl', attributes)
113 | );
114 | }
115 |
116 | const isWebGLSupportedCache = {};
117 | function isWebGLSupportedCached(failIfMajorPerformanceCaveat) {
118 | if (isWebGLSupportedCache[failIfMajorPerformanceCaveat] === undefined) {
119 | isWebGLSupportedCache[failIfMajorPerformanceCaveat] = isWebGLSupported(
120 | failIfMajorPerformanceCaveat,
121 | );
122 | }
123 |
124 | return isWebGLSupportedCache[failIfMajorPerformanceCaveat];
125 | }
126 |
127 | /**
128 | * Create a map in a Promise to handle error properly.
129 | * @return {Promise} A promise that indicates success or fail of creation.
130 | */
131 | export function createMap() {
132 | return new Promise(resolve => {
133 | const body = document.querySelector('body');
134 | const container = document.createElement('div');
135 |
136 | try {
137 | container.id = 'diagnose_map';
138 | body.appendChild(container);
139 |
140 | const map = new mapboxgl.Map({
141 | container: 'diagnose_map',
142 | style: 'mapbox://styles/mapbox/streets-v9',
143 | zoom: 1,
144 | });
145 | map.on('error', event => resolve(event.error.toString()));
146 | map.on('load', () => resolve('ok'));
147 | } catch (e) {
148 | resolve(e.stack || e.tostring);
149 | } finally {
150 | body.removeChild(container);
151 | }
152 | });
153 | }
154 |
155 | /**
156 | * Perform mapbox test
157 | * @param {String} detectionMethod Detection method
158 | * @param {String} contextType Context type
159 | * @param {Boolean} failIfMajorPerformanceCaveat Fail on performance
160 | * @return {Object} Result output
161 | */
162 | function performanceTest(detectionMethod, contextType, failIfMajorPerformanceCaveat) {
163 | const attributes = Object.create(mapboxgl.supported.webGLContextAttributes);
164 | attributes.failIfMajorPerformanceCaveat = failIfMajorPerformanceCaveat;
165 | const canvas = document.createElement('canvas');
166 |
167 | if (canvas[detectionMethod]) {
168 | const result = canvas[detectionMethod](contextType, attributes);
169 | return {
170 | result,
171 | error: result && result.getError && result.getError(),
172 | };
173 | }
174 |
175 | return {
176 | result: false,
177 | error: 'detection method unavailable',
178 | };
179 | }
180 |
181 | export function performanceTests() {
182 | return {
183 | probablySupportsContext_webgl_true: performanceTest(
184 | 'probablySupportsContext',
185 | 'webgl',
186 | true,
187 | ),
188 | probablySupportsContext_webgl_false: performanceTest(
189 | 'probablySupportsContext',
190 | 'webgl',
191 | false,
192 | ),
193 | probablySupportsContext_experimental_true: performanceTest(
194 | 'probablySupportsContext',
195 | 'experimental-webgl',
196 | true,
197 | ),
198 | probablySupportsContext_experimental_false: performanceTest(
199 | 'probablySupportsContext',
200 | 'experimental-webgl',
201 | false,
202 | ),
203 | supportsContext_webgl_true: performanceTest('supportsContext', 'webgl', true),
204 | supportsContext_webgl_false: performanceTest('supportsContext', 'webgl', false),
205 | supportsContext_experimental_true: performanceTest(
206 | 'supportsContext',
207 | 'experimental-webgl',
208 | true,
209 | ),
210 | supportsContext_experimental_false: performanceTest(
211 | 'supportsContext',
212 | 'experimental-webgl',
213 | false,
214 | ),
215 | getContext_webgl_true: performanceTest('getContext', 'webgl', true),
216 | getContext_webgl_false: performanceTest('getContext', 'webgl', false),
217 | getContext_experimental_true: performanceTest('getContext', 'experimental-webgl', true),
218 | getContext_experimental_false: performanceTest('getContext', 'experimental-webgl', false),
219 | };
220 | }
221 |
222 | function isSupported(options) {
223 | return {
224 | isBrowser: isBrowser(),
225 | isArraySupported: isArraySupported(),
226 | isFunctionSupported: isFunctionSupported(),
227 | isObjectSupported: isObjectSupported(),
228 | isJSONSupported: isJSONSupported(),
229 | isWorkerSupported: isWorkerSupported(),
230 | isUint8ClampedArraySupported: isUint8ClampedArraySupported(),
231 | isArrayBufferSupported: isArrayBufferSupported(),
232 | isWebGLSupportedCached: isWebGLSupportedCached(
233 | options && options.failIfMajorPerformanceCaveat,
234 | ),
235 | };
236 | }
237 |
238 | function fullDiagnostic() {
239 | return createMap().then(result => ({
240 | supported: mapboxgl.supported(),
241 | isSupported: isSupported(),
242 | createMap: result,
243 | performance: performanceTests(),
244 | navigator: {
245 | appCodeName: navigator.appCodeName,
246 | appVersion: navigator.appVersion,
247 | doNotTrack: navigator.doNotTrack,
248 | platform: navigator.platform,
249 | userAgent: navigator.userAgent,
250 | webdriver: navigator.webdriver,
251 | language: navigator.language,
252 | },
253 | }));
254 | }
255 |
256 | export default {
257 | createMap,
258 | performanceTests,
259 | isSupported,
260 | fullDiagnostic,
261 | };
262 |
--------------------------------------------------------------------------------
/src/Helpers/index.js:
--------------------------------------------------------------------------------
1 | import circle from '@turf/circle';
2 | import PropTypes from 'prop-types';
3 | import { isFunction } from 'Utils';
4 | import mapboxgl from 'Lib';
5 |
6 | /**
7 | * Props describing a coordinates
8 | * @type {Object}
9 | */
10 | export const LngLatLike = PropTypes.oneOfType([
11 | PropTypes.shape({
12 | lat: PropTypes.number.isRequired,
13 | lng: PropTypes.number.isRequired,
14 | }),
15 | PropTypes.shape({
16 | lat: PropTypes.number.isRequired,
17 | lon: PropTypes.number.isRequired,
18 | }),
19 | PropTypes.arrayOf(PropTypes.number.isRequired),
20 | ]);
21 |
22 | /**
23 | * Number of points to draw a circle.
24 | * @type {Number}
25 | */
26 | const CIRCLE_POINTS_CONFIG = 64;
27 |
28 | /**
29 | * Array of possible units
30 | * @type {Array}
31 | */
32 | export const UNITS = ['kilometers', 'meters', 'miles', 'feet'];
33 |
34 | /**
35 | * Convert the radius on given unit to a compatible unit for turf/circle
36 | * @param {Number} radius of given unit
37 | * @param {String} unit of given radius
38 | */
39 | export function convertRadiusUnit(radius, unit = 'kilometers') {
40 | let convertedRadius = Number(radius);
41 | let convertedUnit = unit;
42 |
43 | if (!Number.isFinite(radius)) {
44 | global.console.error('The radius given is not a number');
45 | }
46 |
47 | if (UNITS.indexOf(unit) === -1) {
48 | // eslint-disable-next-line prefer-destructuring
49 | convertedUnit = UNITS[0];
50 | global.console.warn(
51 | `The unit "${unit}" is not supported, the fallback "${convertedUnit}" is used`,
52 | );
53 | }
54 |
55 | if (unit === 'meters') {
56 | convertedRadius = radius / 1000;
57 | convertedUnit = 'kilometers';
58 | } else if (unit === 'feet') {
59 | convertedRadius = radius / 5280;
60 | convertedUnit = 'miles';
61 | }
62 |
63 | return { radius: convertedRadius, unit: convertedUnit };
64 | }
65 |
66 | /**
67 | * Parse given argument as a coordinates
68 | * @param {Any} coord Value representing a LngLatLike
69 | * @return {Object} A comparable coordinates
70 | */
71 | function parseCoordinates(coord) {
72 | if (Array.isArray(coord) && coord.length === 2) {
73 | return {
74 | lng: coord[0],
75 | lat: coord[1],
76 | };
77 | }
78 |
79 | if (coord instanceof Object && coord !== null) {
80 | return {
81 | lng: coord.lng || coord.lon,
82 | lat: coord.lat,
83 | };
84 | }
85 |
86 | return {};
87 | }
88 |
89 | /**
90 | * Check if coordinates are equal.
91 | * @param {Object} a First coordinates
92 | * @param {Object} b Second coordinates
93 | * @return {Boolean} True if they are equal, false otherwise
94 | */
95 | export function coordinatesAreEqual(a, b) {
96 | const aCoord = parseCoordinates(a);
97 | const bCoord = parseCoordinates(b);
98 |
99 | return aCoord.lat === bCoord.lat && aCoord.lng === bCoord.lng;
100 | }
101 |
102 | /**
103 | * Return new empty bounds according to underlying map library.
104 | * @param {Object} sw South West Bound.
105 | * @param {Object} ne North East Bound.
106 | * @return {Object} Bounds
107 | */
108 | export function newBounds(sw, ne) {
109 | return new mapboxgl.LngLatBounds(sw, ne);
110 | }
111 |
112 | /**
113 | * Create new bound according to the underlying library.
114 | * @param {Object} coordinates Coordinates of bound.
115 | * @return {BoundObject} Bound object matching the underlying map library
116 | */
117 | export function newBound(coordinates) {
118 | const safeCoordinates = parseCoordinates(coordinates);
119 |
120 | return [safeCoordinates.lng, safeCoordinates.lat];
121 | }
122 |
123 | /**
124 | * Get circle points.
125 | * @param {Object} coordinates Center coordinates
126 | * @param {Number} radius Radius
127 | * @param {String} unit Unit of the radius
128 | * @return {Object} GeoJSON data
129 | */
130 | export function getCircleData(coordinates, radius, unit) {
131 | const { radius: usedRadius, unit: usedUnit } = convertRadiusUnit(radius, unit);
132 |
133 | return circle(newBound(coordinates), usedRadius, {
134 | steps: CIRCLE_POINTS_CONFIG,
135 | units: usedUnit,
136 | });
137 | }
138 |
139 | /**
140 | * Get layer identifier from source identifier.
141 | * @param {String} id Source's identifier
142 | * @return {String} Layer's identifier
143 | */
144 | export function getLayerId(id) {
145 | return `${id}_layer`;
146 | }
147 |
148 | /**
149 | * Draw GeoJSON on map.
150 | * @param {Object} map MapBox map
151 | * @param {String} id Identifier of layer
152 | * @param {Object} data Layer data
153 | * @param {Object} paint Paint option of polygon
154 | * @param {Function} onClick Add on click to the layer
155 | */
156 | export function drawGeoJSON(map, id, data, paint = {}, onClick, type = 'fill') {
157 | if (!id || !map || !data) {
158 | return;
159 | }
160 |
161 | const layerId = getLayerId(id);
162 | const source = map.getSource(id);
163 |
164 | if (source) {
165 | source.setData(data);
166 | Object.keys(paint).forEach(property =>
167 | map.setPaintProperty(layerId, property, paint[property]),
168 | );
169 | } else {
170 | map.addSource(id, {
171 | type: 'geojson',
172 | data,
173 | });
174 |
175 | map.addLayer({
176 | id: layerId,
177 | type,
178 | source: id,
179 | layout: {},
180 | paint,
181 | });
182 |
183 | if (isFunction(onClick)) {
184 | map.on('click', layerId, onClick);
185 |
186 | /* eslint-disable no-param-reassign */
187 | map.on('mouseenter', layerId, () => {
188 | map.getCanvas().style.cursor = 'pointer';
189 | });
190 | map.on('mouseleave', layerId, () => {
191 | map.getCanvas().style.cursor = '';
192 | });
193 | /* eslint-enable no-param-reassign */
194 | }
195 | }
196 | }
197 |
198 | /**
199 | * Remove GeoJSON from map.
200 | * @param {Object} map MapBox map
201 | * @param {String} id Identifier of layer
202 | */
203 | export function removeGeoJSON(map, id) {
204 | if (!id || !map) {
205 | return;
206 | }
207 |
208 | const layerId = getLayerId(id);
209 | try {
210 | const layer = map.getLayer(layerId);
211 | if (layer) {
212 | map.removeLayer(layerId);
213 | }
214 | } catch (e) {
215 | global.console.warn(
216 | `Error while removing GeoJSON layer with id ${layerId}. It's sometimes due to an already removed map`,
217 | e,
218 | );
219 | }
220 |
221 | try {
222 | const source = map.getSource(id);
223 | if (source) {
224 | map.removeSource(id);
225 | }
226 | } catch (e) {
227 | global.console.warn(
228 | `Error while removing GeoJSON soruce with id ${id}. It's sometimes due to an already removed map`,
229 | e,
230 | );
231 | }
232 | }
233 |
234 | export default {
235 | convertRadiusUnit,
236 | coordinatesAreEqual,
237 | drawGeoJSON,
238 | getCircleData,
239 | getLayerId,
240 | newBound,
241 | newBounds,
242 | removeGeoJSON,
243 | };
244 |
--------------------------------------------------------------------------------
/src/Helpers/index.test.js:
--------------------------------------------------------------------------------
1 | import sinon from 'sinon';
2 | import chai from 'chai';
3 | import sinonChai from 'sinon-chai';
4 | import {
5 | drawGeoJSON,
6 | removeGeoJSON,
7 | getLayerId,
8 | convertRadiusUnit,
9 | coordinatesAreEqual,
10 | } from './index';
11 |
12 | chai.use(sinonChai);
13 |
14 | describe('coordinatesAreEqual', () => {
15 | it('should handle undefined params', () => {
16 | expect(coordinatesAreEqual()).to.equal(true);
17 | });
18 |
19 | it('should handle null params', () => {
20 | expect(coordinatesAreEqual(null, null)).to.equal(true);
21 | });
22 |
23 | it('should handle different params', () => {
24 | expect(coordinatesAreEqual([10, 20], null)).to.equal(false);
25 | });
26 |
27 | it('should handle array params', () => {
28 | expect(coordinatesAreEqual([10, 20], [10, 20])).to.equal(true);
29 | });
30 |
31 | it('should handle mixed params', () => {
32 | expect(coordinatesAreEqual([10, 20], { lng: 10, lat: 20 })).to.equal(true);
33 | });
34 |
35 | it('should handle lon value', () => {
36 | expect(coordinatesAreEqual({ lng: 10, lat: 20 }, { lon: 10, lat: 20 })).to.equal(true);
37 | });
38 |
39 | it('should compare both value', () => {
40 | expect(coordinatesAreEqual({ lng: 10, lat: 20 }, { lng: 10, lat: 19.999999 })).to.equal(
41 | false,
42 | );
43 | });
44 |
45 | it('should compare both value', () => {
46 | expect(coordinatesAreEqual({ lng: 9.99999, lat: 20 }, { lng: 10, lat: 20 })).to.equal(
47 | false,
48 | );
49 | });
50 | });
51 |
52 | describe('drawGeoJSON', () => {
53 | it('should do nothing if no map', () => {
54 | expect(drawGeoJSON()).to.equal(undefined);
55 | });
56 |
57 | it('should do nothing if no id', () => {
58 | expect(drawGeoJSON({})).to.equal(undefined);
59 | });
60 |
61 | it('should do nothing if no data', () => {
62 | expect(drawGeoJSON({}, 8000)).to.equal(undefined);
63 | });
64 |
65 | it('should add source and layer if not present', () => {
66 | const map = {
67 | getSource: sinon.fake.returns(undefined),
68 | addSource: sinon.spy(),
69 | addLayer: sinon.spy(),
70 | };
71 |
72 | drawGeoJSON(map, 8000, {});
73 | expect(map.getSource.called).to.equal(true);
74 | expect(map.addSource.called).to.equal(true);
75 | expect(map.addLayer.called).to.equal(true);
76 | });
77 |
78 | it('should add event handler if onClick provided', () => {
79 | const map = {
80 | getSource: sinon.fake.returns(undefined),
81 | addSource: sinon.spy(),
82 | addLayer: sinon.spy(),
83 | on: sinon.spy(),
84 | };
85 |
86 | const onClick = () => null;
87 |
88 | drawGeoJSON(map, 8000, {}, {}, onClick);
89 | expect(map.on.calledWith('click', getLayerId(8000), onClick)).to.equal(true);
90 | expect(map.on.callCount).to.equal(3);
91 | });
92 |
93 | it('should update data and paint if already present', () => {
94 | const source = {
95 | setData: sinon.spy(),
96 | };
97 |
98 | const map = {
99 | getSource: sinon.fake.returns(source),
100 | addSource: sinon.spy(),
101 | addLayer: sinon.spy(),
102 | setPaintProperty: sinon.spy(),
103 | on: sinon.spy(),
104 | };
105 |
106 | drawGeoJSON(map, 8000, {}, { 'fill-color': 'blue' });
107 | expect(source.setData.called).to.equal(true);
108 | expect(map.setPaintProperty.called).to.equal(true);
109 | });
110 | });
111 |
112 | describe('removeGeoJSON', () => {
113 | it('should do nothing if no map', () => {
114 | expect(removeGeoJSON()).to.equal(undefined);
115 | });
116 |
117 | it('should do nothing if no id', () => {
118 | expect(removeGeoJSON({})).to.equal(undefined);
119 | });
120 |
121 | it('should do nothing if no source and layer', () => {
122 | const map = {
123 | getLayer: sinon.spy(),
124 | removeLayer: sinon.spy(),
125 | getSource: sinon.spy(),
126 | removeSource: sinon.spy(),
127 | };
128 |
129 | removeGeoJSON(map, 8000);
130 | expect(map.getLayer.called).to.equal(true);
131 | expect(map.removeLayer.called).to.equal(false);
132 | expect(map.getSource.called).to.equal(true);
133 | expect(map.removeSource.called).to.equal(false);
134 | });
135 |
136 | it('should remove layer and source', () => {
137 | const map = {
138 | getLayer: sinon.fake.returns({}),
139 | removeLayer: sinon.spy(),
140 | getSource: sinon.fake.returns({}),
141 | removeSource: sinon.spy(),
142 | };
143 |
144 | removeGeoJSON(map, 8000);
145 | expect(map.getLayer.called).to.equal(true);
146 | expect(map.removeLayer.called).to.equal(true);
147 | expect(map.getSource.called).to.equal(true);
148 | expect(map.removeSource.called).to.equal(true);
149 | });
150 | });
151 |
152 | describe('convertRadiusUnit', () => {
153 | beforeEach(() => {
154 | sinon.spy(console, 'error');
155 | sinon.spy(console, 'warn');
156 | });
157 |
158 | afterEach(() => {
159 | global.console.error.restore();
160 | global.console.warn.restore();
161 | });
162 |
163 | it('should display error "The radius given is not a number" when no radius is given', () => {
164 | convertRadiusUnit();
165 | expect(global.console.error.calledWith('The radius given is not a number')).to.equal(true);
166 | });
167 |
168 | it('should display error "The radius given is not a number" when the radius given isNaN', () => {
169 | convertRadiusUnit('NotANumber');
170 | expect(global.console.error.calledWith('The radius given is not a number')).to.equal(true);
171 | });
172 |
173 | it('should convert radius 15 with a bad unit to 15 km and display warn', () => {
174 | expect(convertRadiusUnit(15, 'notValidUnit')).to.deep.equal({
175 | radius: 15,
176 | unit: 'kilometers',
177 | });
178 | expect(
179 | global.console.warn.calledWith(
180 | 'The unit "notValidUnit" is not supported, the fallback "kilometers" is used',
181 | ),
182 | ).to.equal(true);
183 | });
184 |
185 | it('should convert radius 15 with no unit to 15 km', () => {
186 | expect(convertRadiusUnit(15)).to.deep.equal({ radius: 15, unit: 'kilometers' });
187 | });
188 |
189 | it('should convert radius "15" in string with no unit to 15 km', () => {
190 | expect(convertRadiusUnit('15')).to.deep.equal({ radius: 15, unit: 'kilometers' });
191 | });
192 |
193 | it('should convert radius 15 with unit kilometers to 15 km', () => {
194 | expect(convertRadiusUnit(15, 'kilometers')).to.deep.equal({
195 | radius: 15,
196 | unit: 'kilometers',
197 | });
198 | });
199 |
200 | it('should convert radius 15 with unit meters to 0.015 km', () => {
201 | expect(convertRadiusUnit(15, 'meters')).to.deep.equal({
202 | radius: 0.015,
203 | unit: 'kilometers',
204 | });
205 | });
206 |
207 | it('should convert radius 5280 with unit foot to 1 miles', () => {
208 | expect(convertRadiusUnit(5280, 'feet')).to.deep.equal({ radius: 1, unit: 'miles' });
209 | });
210 |
211 | it('should convert radius 15 with unit miles to 15 miles', () => {
212 | expect(convertRadiusUnit(15, 'miles')).to.deep.equal({ radius: 15, unit: 'miles' });
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/src/Lib/index.js:
--------------------------------------------------------------------------------
1 | import { isBrowser } from 'Utils';
2 |
3 | /**
4 | * Mapbox-gl API.
5 | * @type {Object}
6 | */
7 | // eslint-disable-next-line global-require
8 | export default isBrowser() ? require('mapbox-gl') : {};
9 |
--------------------------------------------------------------------------------
/src/MapboxMap/index.css:
--------------------------------------------------------------------------------
1 | .mapboxgl-map .mapboxgl-canvas {
2 | top: 0;
3 | left: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/src/MapboxMap/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { coordinatesAreEqual, LngLatLike } from 'Helpers';
4 | import { isFunction } from 'Utils';
5 | import mapboxgl from 'Lib';
6 | import 'mapbox-gl/dist/mapbox-gl.css';
7 | import './index.css';
8 |
9 | /**
10 | * Mapbox map navigation methods
11 | * @type {Array}
12 | */
13 | const navigationMethods = ['jumpTo', 'easeTo', 'flyTo'];
14 |
15 | /**
16 | * Debounce timeout on zoom change.
17 | * @type {Number}
18 | */
19 | export const DEBOUNCE_TIMEOUT = 300;
20 |
21 | /**
22 | * Default style for map.
23 | * @type {String}
24 | */
25 | export const DEFAULT_STYLE = 'mapbox://styles/mapbox/streets-v10';
26 |
27 | /**
28 | * MapboxMap Component.
29 | * cf. https://www.mapbox.com/mapbox-gl-js/api/
30 | */
31 | export default class MapboxMap extends Component {
32 | /**
33 | * Creates an instance of MapboxMap.
34 | * @param {Object} props Component props
35 | */
36 | constructor(props) {
37 | super(props);
38 |
39 | const { accessToken } = this.props;
40 | mapboxgl.accessToken = accessToken;
41 |
42 | /**
43 | * Store Mapbox map instance
44 | * @type {mapboxgl.Map}
45 | * @private
46 | */
47 | this.map = null;
48 |
49 | /**
50 | * Zoom end event debounce timeout.
51 | * @type {Number}
52 | * @private
53 | */
54 | this.zoomendTimeout = null;
55 |
56 | this.onChange = this.onChange.bind(this);
57 | this.onZoomEnd = this.onZoomEnd.bind(this);
58 | this.initMap = this.initMap.bind(this);
59 | this.addControls = this.addControls.bind(this);
60 | this.addEvents = this.addEvents.bind(this);
61 | }
62 |
63 | /**
64 | * React lifecycle.
65 | * @param {Object} prevProps Previous props
66 | */
67 | componentDidUpdate(prevProps) {
68 | if (!this.map) {
69 | return;
70 | }
71 |
72 | const { coordinates, zoom, minZoom, maxZoom, mapboxStyle, navigationType } = this.props;
73 | const {
74 | coordinates: prevCenter,
75 | zoom: prevZoom,
76 | minZoom: prevMinZoom,
77 | maxZoom: prevMaxZoom,
78 | mapboxStyle: prevMapboxStyle,
79 | } = prevProps;
80 |
81 | if (!coordinatesAreEqual(coordinates, prevCenter)) {
82 | const mapNavigationType = navigationType.type || 'jumpTo';
83 |
84 | if (navigationMethods.indexOf(mapNavigationType) >= 0) {
85 | const navigationDetails = { center: [coordinates.lng, coordinates.lat] };
86 |
87 | if (mapNavigationType === 'flyTo') {
88 | navigationDetails.curve = navigationType.options && navigationType.options.curve
89 | ? navigationType.options.curve
90 | : 1.42;
91 |
92 | navigationDetails.speed = navigationType.options && navigationType.options.speed
93 | ? navigationType.options.speed
94 | : 1.2;
95 | }
96 |
97 | this.map[mapNavigationType](navigationDetails);
98 | }
99 | }
100 |
101 | if (zoom !== prevZoom) {
102 | this.map.setZoom(zoom);
103 | }
104 |
105 | if (minZoom !== prevMinZoom) {
106 | this.map.setMinZoom(minZoom);
107 | }
108 |
109 | if (maxZoom !== prevMaxZoom) {
110 | this.map.setMaxZoom(maxZoom);
111 | }
112 |
113 | if (mapboxStyle !== prevMapboxStyle) {
114 | this.map.setStyle(mapboxStyle);
115 | }
116 | }
117 |
118 | /**
119 | * React lifecycle.
120 | */
121 | componentWillUnmount() {
122 | if (this.map) {
123 | this.map.remove();
124 | }
125 | }
126 |
127 | /**
128 | * Call callback when changing data on map.
129 | */
130 | onChange() {
131 | const { onChange } = this.props;
132 |
133 | if (isFunction(onChange)) {
134 | const { lng, lat } = this.map.getCenter();
135 |
136 | onChange({
137 | zoom: this.map.getZoom(),
138 | coordinates: { lng, lat },
139 | }, this.map);
140 | }
141 | }
142 |
143 | /**
144 | * Call callback on zoom end after a small debounce.
145 | * @param {Object} e Event object
146 | */
147 | onZoomEnd(e) {
148 | clearTimeout(this.zoomendTimeout);
149 |
150 | /**
151 | * Zoomend debounce timeout.
152 | * @type {Object}
153 | */
154 | this.zoomendTimeout = setTimeout(() => {
155 | this.onChange();
156 |
157 | const { onZoomEnd } = this.props;
158 | if (isFunction(onZoomEnd)) {
159 | onZoomEnd(e, this.map);
160 | }
161 | }, DEBOUNCE_TIMEOUT);
162 | }
163 |
164 | /**
165 | * Initialize map's container from ref.
166 | * @param {DOMElement} container Map's container ref
167 | */
168 | initMap(container) {
169 | if (!container) {
170 | return;
171 | }
172 |
173 | const { coordinates, onLoad, mapboxStyle, ...mapOptions } = this.props;
174 |
175 | this.map = new mapboxgl.Map({
176 | container,
177 | center: new mapboxgl.LngLat(coordinates.lng, coordinates.lat),
178 | style: mapboxStyle,
179 | ...mapOptions,
180 | });
181 |
182 | this.map.on('load', () => {
183 | if (isFunction(onLoad)) {
184 | onLoad(this.map);
185 | }
186 | });
187 |
188 | this.addControls();
189 | this.addEvents();
190 | }
191 |
192 | /**
193 | * Add controls to map according to component's props.
194 | */
195 | addControls() {
196 | const {
197 | fullscreenControlPosition,
198 | navigationControlPosition,
199 | withZoom,
200 | withCompass,
201 | withFullscreen,
202 | } = this.props;
203 |
204 | if (withZoom || withCompass) {
205 | this.map.addControl(
206 | new mapboxgl.NavigationControl({ showCompass: withCompass, showZoom: withZoom }),
207 | navigationControlPosition,
208 | );
209 | }
210 |
211 | if (withFullscreen) {
212 | this.map.addControl(new mapboxgl.FullscreenControl(), fullscreenControlPosition);
213 | }
214 | }
215 |
216 | /**
217 | * Add events handling to map according to component's props callback.
218 | */
219 | addEvents() {
220 | const { onChange, onZoomStart, onZoomEnd, onClick } = this.props;
221 |
222 | if (isFunction(onChange)) {
223 | this.map.on('moveend', this.onChange);
224 | }
225 |
226 | if (isFunction(onZoomStart)) {
227 | this.map.on('zoomstart', onZoomStart);
228 | }
229 |
230 | if (isFunction(onChange) || isFunction(onZoomEnd)) {
231 | this.map.on('zoomend', this.onZoomEnd);
232 | }
233 |
234 | if (isFunction(onClick)) {
235 | this.map.on('click', onClick);
236 | }
237 | }
238 |
239 | /**
240 | * React lifecycle.
241 | */
242 | render() {
243 | const { renderNotSupported, className, children } = this.props;
244 |
245 | if (!mapboxgl.supported() && isFunction(renderNotSupported)) {
246 | return renderNotSupported(className);
247 | }
248 |
249 | return (
250 |
255 | {children}
256 |
257 | );
258 | }
259 | }
260 |
261 | MapboxMap.propTypes = {
262 | accessToken: PropTypes.string,
263 | coordinates: LngLatLike.isRequired,
264 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
265 | className: PropTypes.string,
266 | fullscreenControlPosition: PropTypes.string,
267 | maxZoom: PropTypes.number,
268 | minZoom: PropTypes.number,
269 | navigationControlPosition: PropTypes.string,
270 | navigationType: PropTypes.shape({
271 | type: PropTypes.oneOf(navigationMethods),
272 | options: PropTypes.shape({ speed: PropTypes.number, curve: PropTypes.number }),
273 | }),
274 | onChange: PropTypes.func,
275 | onClick: PropTypes.func,
276 | onLoad: PropTypes.func,
277 | onZoomEnd: PropTypes.func,
278 | onZoomStart: PropTypes.func,
279 | renderNotSupported: PropTypes.func,
280 | mapboxStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
281 | withCompass: PropTypes.bool,
282 | withFullscreen: PropTypes.bool,
283 | withZoom: PropTypes.bool,
284 | zoom: PropTypes.number,
285 | };
286 |
287 | MapboxMap.defaultProps = {
288 | accessToken: '',
289 | children: null,
290 | className: '',
291 | fullscreenControlPosition: 'top-right',
292 | maxZoom: undefined,
293 | minZoom: undefined,
294 | navigationControlPosition: 'bottom-right',
295 | onChange: undefined,
296 | onClick: undefined,
297 | onLoad: undefined,
298 | onZoomEnd: undefined,
299 | onZoomStart: undefined,
300 | mapboxStyle: DEFAULT_STYLE,
301 | navigationType: { type: 'jumpTo' },
302 | withCompass: false,
303 | withFullscreen: false,
304 | withZoom: false,
305 | zoom: 15,
306 | renderNotSupported: className => (
307 |
317 |
326 | Your browser does not support Mapbox GL
327 |
328 |
329 | ),
330 | };
331 |
--------------------------------------------------------------------------------
/src/MapboxMap/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import sinon from 'sinon';
4 | import mapboxgl from 'Lib';
5 | import MapboxMap from './index';
6 |
7 | function defaultProps() {
8 | return {
9 | coordinates: {
10 | lat: 20,
11 | lng: 20,
12 | },
13 | zoom: 15,
14 | };
15 | }
16 |
17 | describe(' ', () => {
18 | let clock;
19 |
20 | before(() => {
21 | clock = sinon.useFakeTimers();
22 | });
23 |
24 | after(() => {
25 | clock.restore();
26 | });
27 |
28 | beforeEach(() => {
29 | /* eslint-disable no-undef */
30 | mapboxgl.Map = class MapMock {
31 | on = sinon.spy();
32 |
33 | addControl = sinon.spy();
34 |
35 | addLayer = sinon.spy();
36 |
37 | easeTo = sinon.spy();
38 |
39 | flyTo = sinon.spy();
40 |
41 | getCenter = sinon.fake.returns({ lat: 20, lng: 20 });
42 |
43 | jumpTo = sinon.spy();
44 |
45 | setCenter = sinon.spy();
46 |
47 | getZoom = sinon.fake.returns(15);
48 |
49 | setZoom = sinon.spy();
50 |
51 | setMinZoom = sinon.spy();
52 |
53 | setMaxZoom = sinon.spy();
54 |
55 | setStyle = sinon.spy();
56 |
57 | remove = sinon.spy();
58 | };
59 |
60 | mapboxgl.LngLat = class LngLatMock {};
61 | mapboxgl.NavigationControl = class NavigationControlMock {};
62 | mapboxgl.FullscreenControl = class FullscreenControlMock {};
63 |
64 | mapboxgl.supported = sinon.fake.returns(true);
65 | /* eslint-enable no-undef */
66 | });
67 |
68 | afterEach(() => {
69 | delete mapboxgl.Map;
70 | delete mapboxgl.LngLat;
71 | delete mapboxgl.NavigationControl;
72 | delete mapboxgl.FullscreenControl;
73 | delete mapboxgl.supported;
74 | });
75 |
76 | it('should render a message when not compatible', () => {
77 | const props = defaultProps();
78 | mapboxgl.supported = sinon.fake.returns(false);
79 |
80 | const wrapper = shallow( );
81 | expect(wrapper.type()).to.equal('div');
82 | expect(
83 | wrapper.findWhere(e => e.text() === 'Your browser does not support Mapbox GL').length >=
84 | 1,
85 | ).to.equal(true);
86 | });
87 |
88 | it('should always render as a div', () => {
89 | const props = defaultProps();
90 | const wrapper = shallow( );
91 | expect(wrapper.type()).to.equal('div');
92 | });
93 |
94 | it('should init map on mount', () => {
95 | const props = defaultProps();
96 | const wrapper = mount( );
97 |
98 | expect(wrapper.instance().map.on.called).to.equal(true);
99 | });
100 |
101 | it('should register events for propagating underlying behavior', () => {
102 | const props = defaultProps();
103 | const wrapper = mount( );
104 |
105 | expect(wrapper.instance().map.on.callCount).to.equal(1);
106 | });
107 |
108 | it('should add control if compass is asked', () => {
109 | const props = defaultProps();
110 | const wrapper = mount( );
111 |
112 | expect(wrapper.instance().map.addControl.called).to.equal(true);
113 | });
114 |
115 | it('should add control if zoom is asked', () => {
116 | const props = defaultProps();
117 | const wrapper = mount( );
118 |
119 | expect(wrapper.instance().map.addControl.called).to.equal(true);
120 | });
121 |
122 | it('should add control with navigation control position if asked', () => {
123 | const props = defaultProps();
124 | const wrapper = mount(
125 | ,
126 | );
127 |
128 | expect(
129 | wrapper
130 | .instance()
131 | .map.addControl.calledWith(
132 | new mapboxgl.NavigationControl({ showCompass: true, showZoom: true }),
133 | 'top-right',
134 | ),
135 | ).to.equal(true);
136 | });
137 |
138 | it('should add control if fullscreen is asked', () => {
139 | const props = defaultProps();
140 | const wrapper = mount( );
141 |
142 | expect(wrapper.instance().map.addControl.called).to.equal(true);
143 | });
144 |
145 | it('should add control with fullscreen control position if asked', () => {
146 | const props = defaultProps();
147 | const wrapper = mount(
148 | ,
149 | );
150 |
151 | expect(
152 | wrapper
153 | .instance()
154 | .map.addControl.calledWith(new mapboxgl.FullscreenControl(), 'top-right'),
155 | ).to.equal(true);
156 | });
157 |
158 | it('should call given onChange', () => {
159 | const onLoad = sinon.spy();
160 |
161 | const props = defaultProps();
162 | const wrapper = mount( );
163 |
164 | // Simulate moveend call from mapbox
165 | const callback = wrapper
166 | .instance()
167 | .map.on.getCalls()
168 | .find(call => call.args[0] === 'load');
169 | callback.args[1]();
170 |
171 | expect(onLoad.called).to.equal(true);
172 | });
173 |
174 | it('should call given onChange', () => {
175 | const onChange = sinon.spy();
176 |
177 | const props = defaultProps();
178 | const wrapper = mount( );
179 |
180 | // Simulate moveend call from mapbox
181 | const callback = wrapper.instance().map.on.getCalls()
182 | .find(call => call.args[0] === 'moveend');
183 | callback.args[1]();
184 |
185 | expect(
186 | onChange.calledWith(
187 | {
188 | zoom: wrapper.instance().map.getZoom(),
189 | coordinates: wrapper.instance().map.getCenter(),
190 | },
191 | wrapper.instance().map,
192 | ),
193 | ).to.equal(true);
194 | });
195 |
196 | it('should call given onZoomStart', () => {
197 | const onZoomStart = sinon.spy();
198 |
199 | const props = defaultProps();
200 | const wrapper = mount( );
201 |
202 | // Simulate zoomstart call from mapbox
203 | const callback = wrapper
204 | .instance()
205 | .map.on.getCalls()
206 | .find(call => call.args[0] === 'zoomstart');
207 | callback.args[1]();
208 |
209 | expect(onZoomStart.called).to.equal(true);
210 | });
211 |
212 | it('should call given zoomend', () => {
213 | const onZoomEnd = sinon.spy();
214 |
215 | const props = defaultProps();
216 | const wrapper = mount( );
217 |
218 | // Simulate zoomstart call from mapbox
219 | const callback = wrapper
220 | .instance()
221 | .map.on.getCalls()
222 | .find(call => call.args[0] === 'zoomend');
223 | callback.args[1]();
224 |
225 | clock.tick(1000);
226 |
227 | expect(
228 | onZoomEnd.calledWith(
229 | undefined,
230 | wrapper.instance().map,
231 | ),
232 | ).to.equal(true);
233 | });
234 |
235 | it('should call given click', () => {
236 | const onClick = sinon.spy();
237 |
238 | const props = defaultProps();
239 | const wrapper = mount( );
240 |
241 | // Simulate zoomstart call from mapbox
242 | const callback = wrapper
243 | .instance()
244 | .map.on.getCalls()
245 | .find(call => call.args[0] === 'click');
246 | callback.args[1]();
247 |
248 | expect(onClick.called).to.equal(true);
249 | });
250 |
251 | it('should not update if no change', () => {
252 | const props = defaultProps();
253 | const wrapper = mount( );
254 |
255 | wrapper.setProps({
256 | ...props.coordinates,
257 | minZoom: 5,
258 | maxZoom: 20,
259 | });
260 |
261 | expect(wrapper.instance().map.setCenter.called).to.equal(false);
262 | expect(wrapper.instance().map.setZoom.called).to.equal(false);
263 | expect(wrapper.instance().map.setMinZoom.called).to.equal(false);
264 | expect(wrapper.instance().map.setMaxZoom.called).to.equal(false);
265 | expect(wrapper.instance().map.setStyle.called).to.equal(false);
266 | });
267 |
268 | it('should update lat on change', () => {
269 | const props = defaultProps();
270 | const wrapper = mount( );
271 |
272 | wrapper.setProps({
273 | coordinates: {
274 | ...props.coordinates,
275 | lat: 0,
276 | },
277 | });
278 |
279 | expect(
280 | wrapper.instance().map.jumpTo.calledWith({ center: [props.coordinates.lng, 0] }),
281 | ).to.equal(true);
282 | });
283 |
284 | it('should update lng on change', () => {
285 | const props = defaultProps();
286 | const wrapper = mount( );
287 |
288 | wrapper.setProps({
289 | coordinates: {
290 | ...props.coordinates,
291 | lng: 0,
292 | },
293 | });
294 |
295 | expect(
296 | wrapper.instance().map.jumpTo.calledWith({ center: [0, props.coordinates.lat] }),
297 | ).to.equal(true);
298 | });
299 |
300 | it('should jump to new coordinates', () => {
301 | const props = defaultProps();
302 | const wrapper = mount( );
303 |
304 | wrapper.setProps({
305 | coordinates: {
306 | ...props.coordinates,
307 | lng: 0,
308 | },
309 | navigationType: { type: 'jumpTo' },
310 | });
311 |
312 | expect(
313 | wrapper.instance().map.jumpTo.calledWith({ center: [0, props.coordinates.lat] }),
314 | ).to.equal(true);
315 | });
316 |
317 | it('should ease to new coordinates', () => {
318 | const props = defaultProps();
319 | const wrapper = mount( );
320 |
321 | wrapper.setProps({
322 | coordinates: {
323 | ...props.coordinates,
324 | lng: 0,
325 | },
326 | navigationType: { type: 'easeTo' },
327 | });
328 |
329 | expect(
330 | wrapper.instance().map.easeTo.calledWith({ center: [0, props.coordinates.lat] }),
331 | ).to.equal(true);
332 | });
333 |
334 | it('should fly to new coordinates', () => {
335 | const props = defaultProps();
336 | const wrapper = mount( );
337 |
338 | wrapper.setProps({
339 | coordinates: {
340 | ...props.coordinates,
341 | lng: 0,
342 | },
343 | navigationType: { type: 'flyTo' },
344 | });
345 |
346 | expect(
347 | wrapper.instance().map.flyTo.calledWith({
348 | center: [0, props.coordinates.lat],
349 | curve: 1.42,
350 | speed: 1.2,
351 | }),
352 | ).to.equal(true);
353 | });
354 |
355 | it('should fly to new coordinates with customized curve and speed', () => {
356 | const props = defaultProps();
357 | const wrapper = mount( );
358 |
359 | wrapper.setProps({
360 | coordinates: {
361 | ...props.coordinates,
362 | lng: 0,
363 | },
364 | navigationType: { type: 'flyTo', options: { curve: 2, speed: 2 } },
365 | });
366 |
367 | expect(
368 | wrapper
369 | .instance()
370 | .map.flyTo.calledWith({ center: [0, props.coordinates.lat], curve: 2, speed: 2 }),
371 | ).to.equal(true);
372 | });
373 |
374 | it('should update zoom on change', () => {
375 | const props = defaultProps();
376 | const wrapper = mount( );
377 |
378 | wrapper.setProps({
379 | zoom: 5,
380 | });
381 |
382 | expect(wrapper.instance().map.setZoom.calledWith(5)).to.equal(true);
383 | });
384 |
385 | it('should update minZoom on change', () => {
386 | const props = defaultProps();
387 | const wrapper = mount( );
388 |
389 | wrapper.setProps({
390 | minZoom: 5,
391 | });
392 |
393 | expect(wrapper.instance().map.setMinZoom.calledWith(5)).to.equal(true);
394 | });
395 |
396 | it('should update maxZoom on change', () => {
397 | const props = defaultProps();
398 | const wrapper = mount( );
399 |
400 | wrapper.setProps({
401 | maxZoom: 20,
402 | });
403 |
404 | expect(wrapper.instance().map.setMaxZoom.calledWith(20)).to.equal(true);
405 | });
406 |
407 | it('should update style on change', () => {
408 | const props = defaultProps();
409 | const wrapper = mount( );
410 |
411 | wrapper.setProps({
412 | mapboxStyle: 'test',
413 | });
414 |
415 | expect(wrapper.instance().map.setStyle.calledWith('test')).to.equal(true);
416 | });
417 |
418 | it('should remove from DOM on unmount', () => {
419 | const props = defaultProps();
420 | const wrapper = mount( );
421 | const instance = wrapper.instance();
422 | wrapper.unmount();
423 |
424 | expect(instance.map.remove.called).to.equal(true);
425 | });
426 | });
427 |
--------------------------------------------------------------------------------
/src/Marker/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { isFunction } from 'Utils';
4 | import { coordinatesAreEqual, LngLatLike } from 'Helpers';
5 | import Popup from 'Popup';
6 | import mapboxgl from 'Lib';
7 | import { DEBOUNCE_TIMEOUT } from 'MapboxMap';
8 |
9 | /**
10 | * Marker rendered on map.
11 | */
12 | export default class Marker extends Component {
13 | /**
14 | * Creates an instance of Marker.
15 | * @param {Object} props Component props
16 | */
17 | constructor(props) {
18 | super(props);
19 |
20 | /**
21 | * Timer for mouseover/out debouncing
22 | * @type {Number}
23 | * @private
24 | */
25 | this.overTimeout = 0;
26 |
27 | /**
28 | * Instance of the marker
29 | * @type {Mapbox.Marker}
30 | * @private
31 | */
32 | this.marker = null;
33 |
34 | /**
35 | * Instance of the popup
36 | * @type {Mapbox.Popup}
37 | * @private
38 | */
39 | this.popup = null;
40 |
41 | this.onMarkerOver = this.onMarkerOver.bind(this);
42 | this.onMarkerOut = this.onMarkerOut.bind(this);
43 | this.onDragEnd = this.onDragEnd.bind(this);
44 | this.initMarker = this.initMarker.bind(this);
45 | this.initPopup = this.initPopup.bind(this);
46 | this.clearDebounce = this.clearDebounce.bind(this);
47 | this.moveToTop = this.moveToTop.bind(this);
48 |
49 | /**
50 | * Props for rendered marker (custom or default)
51 | * @type {Object}
52 | */
53 | this.markerProps = {
54 | ref: this.initMarker,
55 | 'data-marker': '',
56 | };
57 | }
58 |
59 | /**
60 | * React lifecycle.
61 | * @param {Object} prevProps Previous props
62 | */
63 | componentDidUpdate(prevProps) {
64 | if (!this.marker) {
65 | return;
66 | }
67 |
68 | const { coordinates: prevCoord } = prevProps;
69 | const { coordinates } = this.props;
70 |
71 | if (!coordinatesAreEqual(coordinates, prevCoord)) {
72 | this.marker.setLngLat(coordinates);
73 | }
74 | }
75 |
76 | /**
77 | * React lifecycle.
78 | */
79 | componentWillUnmount() {
80 | if (!this.marker) {
81 | return;
82 | }
83 |
84 | this.clearDebounce();
85 | this.marker.remove();
86 | }
87 |
88 | /**
89 | * Mouse over on a marker : display popup
90 | * @param {Event} e
91 | */
92 | onMarkerOver(e) {
93 | this.clearDebounce();
94 |
95 | const { popupOnOver, onMouseOver } = this.props;
96 |
97 | if (popupOnOver && this.popup && !this.popup.isOpen()) {
98 | this.marker.togglePopup();
99 | }
100 |
101 | if (isFunction(onMouseOver)) {
102 | onMouseOver(e);
103 | }
104 | }
105 |
106 | /**
107 | * Mouse over on a marker : hide popup
108 | * @param {Event} e
109 | * @use setTimeout for debouncing
110 | */
111 | onMarkerOut(e) {
112 | this.clearDebounce();
113 |
114 | this.overTimeout = setTimeout(() => {
115 | const { popupOnOver, onMouseOut } = this.props;
116 |
117 | if (popupOnOver && this.popup && this.popup.isOpen()) {
118 | this.marker.togglePopup();
119 | }
120 |
121 | if (isFunction(onMouseOut)) {
122 | onMouseOut(e);
123 | }
124 | }, DEBOUNCE_TIMEOUT);
125 | }
126 |
127 | /**
128 | * Call dragend if function is passed from props
129 | */
130 | onDragEnd() {
131 | const { onDragEnd } = this.props;
132 |
133 | if (isFunction(onDragEnd)) {
134 | onDragEnd(this.marker.getLngLat());
135 | }
136 | }
137 |
138 | /**
139 | * Attach to the dom
140 | * @param {Element} element Dom element of the marker
141 | */
142 | initMarker(element) {
143 | if (!element) {
144 | return;
145 | }
146 |
147 | const { coordinates, map, draggable, onDragEnd, getRef, children, ...rest } = this.props;
148 |
149 | this.marker = new mapboxgl.Marker({
150 | element: children ? element : null,
151 | draggable,
152 | ...rest,
153 | });
154 |
155 | this.marker.setLngLat(coordinates);
156 | this.marker.addTo(map);
157 |
158 | if (this.popup) {
159 | this.marker.setPopup(this.popup);
160 | }
161 |
162 | if (draggable && isFunction(onDragEnd)) {
163 | this.marker.on('dragend', this.onDragEnd);
164 | }
165 |
166 | if (isFunction(getRef)) {
167 | getRef(this);
168 | }
169 | }
170 |
171 | /**
172 | * Display a Popup from a marker
173 | * @param {Object} ref React ref element of the marker
174 | */
175 | initPopup(ref) {
176 | if (!ref) {
177 | return;
178 | }
179 |
180 | this.popup = ref.getMapboxPopup();
181 |
182 | if (this.marker) {
183 | this.marker.setPopup(this.popup);
184 | }
185 | }
186 |
187 | /**
188 | * Clear mouseover timeout
189 | */
190 | clearDebounce() {
191 | clearTimeout(this.overTimeout);
192 | }
193 |
194 | /**
195 | * Place marker at the top of layers
196 | */
197 | moveToTop() {
198 | if (this.marker) {
199 | this.marker.remove();
200 | // eslint-disable-next-line react/destructuring-assignment
201 | this.marker.addTo(this.props.map);
202 | }
203 | }
204 |
205 | /**
206 | * React lifecycle.
207 | */
208 | render() {
209 | const { children, popup, popupAnchor, popupOffset, popupCloseButton } = this.props;
210 |
211 | let marker;
212 | let wrapperProps;
213 | if (children) {
214 | marker = (
215 |
223 | {children}
224 |
225 | );
226 | } else {
227 | wrapperProps = this.markerProps;
228 | }
229 |
230 | return (
231 |
232 | {marker}
233 | {popup && (
234 |
245 | {popup}
246 |
247 | )}
248 |
249 | );
250 | }
251 | }
252 |
253 | Marker.propTypes = {
254 | coordinates: LngLatLike.isRequired,
255 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
256 | draggable: PropTypes.bool,
257 | getRef: PropTypes.func,
258 | map: PropTypes.shape({}).isRequired,
259 | onDragEnd: PropTypes.func,
260 | onMouseOut: PropTypes.func,
261 | onMouseOver: PropTypes.func,
262 | popup: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
263 | popupAnchor: PropTypes.string,
264 | popupCloseButton: PropTypes.bool,
265 | popupOffset: PropTypes.number,
266 | popupOnOver: PropTypes.bool,
267 | };
268 |
269 | Marker.defaultProps = {
270 | children: null,
271 | draggable: false,
272 | getRef: undefined,
273 | onDragEnd: undefined,
274 | onMouseOut: undefined,
275 | onMouseOver: undefined,
276 | popup: undefined,
277 | popupAnchor: undefined,
278 | popupCloseButton: false,
279 | popupOffset: undefined,
280 | popupOnOver: false,
281 | };
282 |
--------------------------------------------------------------------------------
/src/Marker/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import sinon from 'sinon';
4 | import mapboxgl from 'Lib';
5 | import { DEBOUNCE_TIMEOUT } from 'MapboxMap';
6 | import Marker from './index';
7 |
8 | /* eslint-disable jsx-a11y/mouse-events-have-key-events */
9 |
10 | function defaultProps() {
11 | return {
12 | coordinates: {
13 | lat: 10,
14 | lng: 10,
15 | },
16 | map: {},
17 | };
18 | }
19 |
20 | describe(' ', () => {
21 | let clock;
22 |
23 | before(() => {
24 | clock = sinon.useFakeTimers();
25 | });
26 |
27 | after(() => {
28 | clock.restore();
29 | });
30 |
31 | beforeEach(() => {
32 | /* eslint-disable no-undef */
33 | mapboxgl.Marker = class MarkerMock {
34 | setLngLat = sinon.spy();
35 |
36 | addTo = sinon.spy();
37 |
38 | setPopup = sinon.spy();
39 |
40 | togglePopup = sinon.spy();
41 |
42 | remove = sinon.spy();
43 |
44 | on = sinon.spy();
45 |
46 | getLngLat = sinon.spy();
47 | };
48 |
49 | mapboxgl.Popup = class PopupMock {
50 | setLngLat = sinon.spy();
51 |
52 | addTo = sinon.spy();
53 |
54 | remove = sinon.spy();
55 |
56 | isOpen = sinon.spy();
57 |
58 | setDOMContent = sinon.spy();
59 | };
60 | /* eslint-enable no-undef */
61 | });
62 |
63 | afterEach(() => {
64 | delete mapboxgl.Marker;
65 | delete mapboxgl.Popup;
66 | });
67 |
68 | it('should set marker coordinates', () => {
69 | const props = defaultProps();
70 | const wrapper = mount( );
71 | expect(wrapper.instance().marker.setLngLat.calledWith({ lat: 10, lng: 10 })).to.equal(true);
72 | });
73 |
74 | it('should add marker to map', () => {
75 | const props = defaultProps();
76 | const wrapper = mount( );
77 | expect(wrapper.instance().marker.addTo.called).to.equal(true);
78 | });
79 |
80 | it('should remove marker on unmount', () => {
81 | const props = defaultProps();
82 | const wrapper = mount( );
83 |
84 | wrapper.instance().componentWillUnmount();
85 | expect(wrapper.instance().marker.remove.called).to.equal(true);
86 | });
87 |
88 | it('should do nothing if nothing change', () => {
89 | const props = defaultProps();
90 | const wrapper = mount( );
91 | wrapper.instance().marker.setLngLat.resetHistory();
92 |
93 | wrapper.setProps();
94 |
95 | expect(wrapper.instance().marker.setLngLat.called).to.equal(false);
96 | });
97 |
98 | it('should update lat coordinates on change', () => {
99 | const props = defaultProps();
100 | const wrapper = mount( );
101 |
102 | wrapper.setProps({
103 | coordinates: {
104 | ...props.coordinates,
105 | lat: 0,
106 | },
107 | });
108 |
109 | expect(
110 | wrapper.instance().marker.setLngLat.calledWith({ lng: props.coordinates.lng, lat: 0 }),
111 | ).to.equal(true);
112 | });
113 |
114 | it('should update lng coordinates on change', () => {
115 | const props = defaultProps();
116 | const wrapper = mount( );
117 |
118 | wrapper.setProps({
119 | coordinates: {
120 | ...props.coordinates,
121 | lng: 0,
122 | },
123 | });
124 |
125 | expect(
126 | wrapper.instance().marker.setLngLat.calledWith({ lat: props.coordinates.lat, lng: 0 }),
127 | ).to.equal(true);
128 | });
129 |
130 | it('should add popup if provided', () => {
131 | const props = defaultProps();
132 | props.popup = Hello World! ;
133 |
134 | const wrapper = mount( );
135 | expect(wrapper.find('Popup').length).to.equal(1);
136 | });
137 |
138 | it('should toggle popup on over if asked', () => {
139 | const props = defaultProps();
140 | props.popup = Hello World! ;
141 |
142 | const wrapper = mount(
143 |
144 | Content
145 | ,
146 | );
147 | wrapper.instance().popup.isOpen = sinon.fake.returns(false);
148 | wrapper.find('[data-marker]').simulate('mouseOver');
149 |
150 | expect(wrapper.instance().marker.togglePopup.called).to.equal(true);
151 | });
152 |
153 | it('should call given callback on over', () => {
154 | const props = defaultProps();
155 | props.popup = Hello World! ;
156 | const onMouseOver = sinon.spy();
157 |
158 | const wrapper = mount(
159 |
160 | Content
161 | ,
162 | );
163 | wrapper.instance().popup.isOpen = sinon.fake.returns(false);
164 | wrapper.find('[data-marker]').simulate('mouseOver');
165 |
166 | expect(onMouseOver.called).to.equal(true);
167 | });
168 |
169 | it('should toggle popup on out if asked', () => {
170 | const props = defaultProps();
171 | props.popup = Hello World! ;
172 |
173 | const wrapper = mount(
174 |
175 | Content
176 | ,
177 | );
178 | wrapper.instance().popup.isOpen = sinon.fake.returns(true);
179 | wrapper.find('[data-marker]').simulate('mouseOut');
180 |
181 | clock.tick(DEBOUNCE_TIMEOUT);
182 |
183 | expect(wrapper.instance().marker.togglePopup.called).to.equal(true);
184 | });
185 |
186 | it('should call given callback on out', () => {
187 | const props = defaultProps();
188 | props.popup = Hello World! ;
189 | const onMouseOut = sinon.spy();
190 |
191 | const wrapper = mount(
192 |
193 | Content
194 | ,
195 | );
196 | wrapper.instance().popup.isOpen = sinon.fake.returns(false);
197 | wrapper.find('[data-marker]').simulate('mouseOut');
198 |
199 | clock.tick(DEBOUNCE_TIMEOUT);
200 |
201 | expect(onMouseOut.called).to.equal(true);
202 | });
203 |
204 | it('should call given callback on dragend', () => {
205 | const props = defaultProps();
206 | const onDragEnd = sinon.spy();
207 |
208 | const wrapper = mount( );
209 | wrapper.instance().marker.getLngLat = sinon.fake.returns(props.coordinates);
210 | wrapper.instance().onDragEnd();
211 |
212 | expect(onDragEnd.calledWith(props.coordinates)).to.equal(true);
213 | });
214 |
215 | it('should remove and add marker for moving to top', () => {
216 | const props = defaultProps();
217 |
218 | const wrapper = mount( );
219 | wrapper.instance().moveToTop();
220 |
221 | expect(wrapper.instance().marker.remove.called).to.equal(true);
222 | expect(wrapper.instance().marker.addTo.called).to.equal(true);
223 | });
224 | });
225 |
--------------------------------------------------------------------------------
/src/Popup/index.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PropTypes from 'prop-types';
4 | import mapboxgl from 'Lib';
5 | import { coordinatesAreEqual, LngLatLike } from 'Helpers';
6 |
7 | /**
8 | * Popup Component.
9 | */
10 | export default class Popup extends Component {
11 | /**
12 | * Creates an instance of Popup.
13 | * @param {Object} props Component props
14 | */
15 | constructor(props) {
16 | super(props);
17 |
18 | /**
19 | * Dom element of the popup container
20 | * @type {Element}
21 | * @private
22 | */
23 | this.container = null;
24 |
25 | /**
26 | * Instance of the popup
27 | * @type {Mapbox.Popup}
28 | * @private
29 | */
30 | this.popup = null;
31 |
32 | const {
33 | anchor,
34 | onMouseOver,
35 | onMouseOut,
36 | closeButton,
37 | closeOnClick,
38 | offset,
39 | className,
40 | coordinates,
41 | map,
42 | } = this.props;
43 |
44 | this.container = document.createElement('div');
45 | this.container.addEventListener('mouseover', onMouseOver);
46 | this.container.addEventListener('mouseout', onMouseOut);
47 |
48 | this.popup = new mapboxgl.Popup({ closeButton, closeOnClick, offset, className, anchor });
49 | this.popup.setDOMContent(this.container);
50 |
51 | if (coordinates) {
52 | this.popup.setLngLat(coordinates);
53 | }
54 |
55 | if (map) {
56 | this.popup.addTo(map);
57 | }
58 |
59 | this.getMapboxPopup = this.getMapboxPopup.bind(this);
60 | }
61 |
62 | /**
63 | * React lifecycle.
64 | * @param {Object} prevProps Previous props
65 | */
66 | componentDidUpdate(prevProps) {
67 | const { coordinates: prevCoord } = prevProps;
68 | const { coordinates } = this.props;
69 |
70 | if (!coordinatesAreEqual(prevCoord || {}, coordinates || {})) {
71 | this.popup.setLngLat(coordinates);
72 | }
73 | }
74 |
75 | /**
76 | * React lifecycle.
77 | */
78 | componentWillUnmount() {
79 | this.popup.remove();
80 | }
81 |
82 | /**
83 | * Accessor of underlying Mapbox Popup
84 | * @return {Element}
85 | */
86 | getMapboxPopup() {
87 | return this.popup;
88 | }
89 |
90 | /**
91 | * React lifecycle.
92 | */
93 | render() {
94 | // eslint-disable-next-line react/destructuring-assignment
95 | return ReactDOM.createPortal(this.props.children, this.container);
96 | }
97 | }
98 |
99 | Popup.propTypes = {
100 | anchor: PropTypes.oneOf([
101 | 'center',
102 | 'top',
103 | 'bottom',
104 | 'left',
105 | 'right',
106 | 'top-left',
107 | 'top-right',
108 | 'bottom-left',
109 | 'bottom-right',
110 | ]),
111 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
112 | className: PropTypes.string,
113 | closeButton: PropTypes.bool,
114 | closeOnClick: PropTypes.bool,
115 | coordinates: LngLatLike,
116 | map: PropTypes.shape({}),
117 | offset: PropTypes.number,
118 | onMouseOut: PropTypes.func,
119 | onMouseOver: PropTypes.func,
120 | };
121 |
122 | Popup.defaultProps = {
123 | anchor: undefined,
124 | children: null,
125 | className: '',
126 | closeButton: true,
127 | closeOnClick: true,
128 | coordinates: undefined,
129 | map: undefined,
130 | offset: undefined,
131 | onMouseOut: undefined,
132 | onMouseOver: undefined,
133 | };
134 |
--------------------------------------------------------------------------------
/src/Popup/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import sinon from 'sinon';
4 | import mapboxgl from 'Lib';
5 | import Popup from './index';
6 |
7 | function defaultProps() {
8 | return {};
9 | }
10 |
11 | describe(' ', () => {
12 | beforeEach(() => {
13 | /* eslint-disable no-undef */
14 | mapboxgl.Popup = class PopupMock {
15 | constructor(options) {
16 | this.constructorSpy(options);
17 | }
18 |
19 | constructorSpy = sinon.spy();
20 |
21 | setLngLat = sinon.spy();
22 |
23 | addTo = sinon.spy();
24 |
25 | remove = sinon.spy();
26 |
27 | setDOMContent = sinon.spy();
28 | };
29 | /* eslint-enable no-undef */
30 | });
31 |
32 | afterEach(() => {
33 | delete mapboxgl.Popup;
34 | });
35 |
36 | it('should set popup coordinates if provided', () => {
37 | const props = defaultProps();
38 | props.coordinates = { lat: 10, lng: 10 };
39 | const wrapper = mount( );
40 |
41 | expect(wrapper.instance().popup.setLngLat.calledWith({ lat: 10, lng: 10 })).to.equal(true);
42 | });
43 |
44 | it('should add popup to map if provided', () => {
45 | const props = defaultProps();
46 | props.map = {};
47 | const wrapper = mount( );
48 |
49 | expect(wrapper.instance().popup.addTo.called).to.equal(true);
50 | });
51 |
52 | it('should remove popup on unmount', () => {
53 | const props = defaultProps();
54 | const wrapper = mount( );
55 | wrapper.instance().componentWillUnmount();
56 |
57 | expect(wrapper.instance().popup.remove.called).to.equal(true);
58 | });
59 |
60 | it('should do nothing if nothing change', () => {
61 | const props = defaultProps();
62 | const wrapper = mount( );
63 |
64 | wrapper.instance().popup.setLngLat.resetHistory();
65 |
66 | wrapper.setProps({});
67 |
68 | expect(wrapper.instance().popup.setLngLat.called).to.equal(false);
69 | });
70 |
71 | it('should update lat coordinates on change', () => {
72 | const props = defaultProps();
73 | props.coordinates = { lat: 10, lng: 10 };
74 | const wrapper = mount( );
75 |
76 | wrapper.setProps({
77 | coordinates: {
78 | ...props.coordinates,
79 | lat: 0,
80 | },
81 | });
82 |
83 | expect(
84 | wrapper.instance().popup.setLngLat.calledWith({ lng: props.coordinates.lng, lat: 0 }),
85 | ).to.equal(true);
86 | });
87 |
88 | it('should update lng coordinates on change', () => {
89 | const props = defaultProps();
90 | props.coordinates = { lat: 10, lng: 10 };
91 | const wrapper = mount( );
92 |
93 | wrapper.setProps({
94 | coordinates: {
95 | ...props.coordinates,
96 | lng: 0,
97 | },
98 | });
99 |
100 | expect(
101 | wrapper.instance().popup.setLngLat.calledWith({ lat: props.coordinates.lat, lng: 0 }),
102 | ).to.equal(true);
103 | });
104 |
105 | it('should not pass anchor option to mapbox popin options if not provided', () => {
106 | const props = defaultProps();
107 | const wrapper = mount( );
108 |
109 | const constructorSpyCall = wrapper.instance().popup.constructorSpy.getCall(0);
110 | expect(constructorSpyCall.args[0].anchor).to.equal(undefined);
111 | });
112 |
113 | it('should pass anchor option to mapbox popin options if provided', () => {
114 | const props = defaultProps();
115 | props.anchor = 'top';
116 | const wrapper = mount( );
117 |
118 | const constructorSpyCall = wrapper.instance().popup.constructorSpy.getCall(0);
119 | expect(constructorSpyCall.args[0].anchor).to.equal('top');
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/src/Utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if two items are deeply equals.
3 | * @param {any} o1 First item
4 | * @param {any} o2 Second item
5 | * @return {bool} True is they are deeply equal, false otherwise
6 | */
7 | export function deepEqual(o1, o2) {
8 | if (o1 === o2) {
9 | return true;
10 | }
11 |
12 | if (typeof o1 !== typeof o2) {
13 | return false;
14 | }
15 |
16 | const typeO1 = typeof o1;
17 | const typeO2 = typeof o2;
18 |
19 | if (typeO1 !== typeO2) {
20 | return false;
21 | }
22 |
23 | if ((o1 === null && o2 !== null) || (o1 !== null && o2 === null)) {
24 | return false;
25 | }
26 |
27 | if (typeO1 === 'number' && Number.isNaN(o1) && Number.isNaN(o2)) {
28 | return true;
29 | }
30 |
31 | if (
32 | typeO1 === 'undefined' ||
33 | typeO1 === 'string' ||
34 | typeO1 === 'number' ||
35 | typeO1 === 'boolean'
36 | ) {
37 | return false;
38 | }
39 |
40 | if (o1 instanceof Date && o2 instanceof Date) {
41 | return o1.getTime() === o2.getTime();
42 | }
43 |
44 | if (Array.isArray(o1)) {
45 | if (o1.length !== o2.length) {
46 | return false;
47 | }
48 |
49 | for (let i = 0, size = o1.length; i < size; i += 1) {
50 | if (!deepEqual(o1[i], o2[i])) {
51 | return false;
52 | }
53 | }
54 | }
55 |
56 | const o1Keys = Object.keys(o1).sort();
57 | const o2Keys = Object.keys(o2).sort();
58 |
59 | if (o1Keys.join(',') !== o2Keys.join(',')) {
60 | return false;
61 | }
62 |
63 | for (let i = 0, size = o1Keys.length; i < size; i += 1) {
64 | const key = o1Keys[i];
65 | if (!deepEqual(o1[key], o2[key])) {
66 | return false;
67 | }
68 | }
69 |
70 | return true;
71 | }
72 |
73 | /**
74 | * Determine wether or not given parameter is a function.
75 | * @param {Any} fn A possible function
76 | * @return {Boolean} True if function, false otherwise
77 | */
78 | export function isFunction(fn) {
79 | return fn instanceof Function;
80 | }
81 |
82 | /**
83 | * Checking if we are in a browser or not.
84 | * @type {Boolean}
85 | */
86 | export function isBrowser() {
87 | return !(Object.prototype.toString.call(global.process) === '[object process]');
88 | }
89 |
90 | export default {
91 | deepEqual,
92 | isFunction,
93 | isBrowser,
94 | };
95 |
--------------------------------------------------------------------------------
/src/Utils/index.test.js:
--------------------------------------------------------------------------------
1 | import { deepEqual } from './index';
2 |
3 | describe('deepEqual', () => {
4 | it('should work with native variable', () => {
5 | expect(deepEqual(1, 1), '1, 1').to.equal(true);
6 | expect(deepEqual(false, false), 'false, false').to.equal(true);
7 | expect(deepEqual('test', 'test'), "test', 'test'").to.equal(true);
8 | expect(deepEqual(null, null), 'null, null').to.equal(true);
9 |
10 | expect(deepEqual(1, 2), '1, 2').to.equal(false);
11 | expect(deepEqual(false, true), 'false, true').to.equal(false);
12 | expect(deepEqual('test', 'tset'), "'test', 'tset'").to.equal(false);
13 | expect(deepEqual(null, undefined), 'null, undefined').to.equal(false);
14 | expect(deepEqual(NaN, -Infinity), 'NaN, -Infinity').to.equal(false);
15 | });
16 |
17 | it('should work when args have different types', () => {
18 | expect(deepEqual(1, '1')).to.equal(false);
19 | });
20 |
21 | it('should work with NaN', () => {
22 | expect(deepEqual(NaN, NaN)).to.equal(true);
23 | });
24 |
25 | it('should work with Date', () => {
26 | expect(deepEqual(new Date(), new Date(15000000))).to.equal(false);
27 | expect(deepEqual(new Date(15000000), new Date(15000000))).to.equal(true);
28 | });
29 |
30 | it('should work with Array according to order', () => {
31 | expect(deepEqual(['a', 'b'], ['a', 'b'])).to.equal(true);
32 | expect(deepEqual(['a', 'b'], ['b', 'a'])).to.equal(false);
33 | expect(deepEqual(['a', 'b'], ['a', 'b', 'c'])).to.equal(false);
34 | });
35 |
36 | it('should work with Object according to order', () => {
37 | expect(
38 | deepEqual(
39 | { key: 'value', tab: ['a', 'b'], bool: false },
40 | { tab: ['a', 'b'], key: 'value', bool: false },
41 | ),
42 | ).to.equal(true);
43 | expect(
44 | deepEqual(
45 | { key: 'value', tab: ['a', 'b'], bool: false },
46 | { tab: ['a', 'c'], key: 'value', bool: false },
47 | ),
48 | ).to.equal(false);
49 | expect(
50 | deepEqual(
51 | { key: 'value', tab: ['a', 'b'], bool: false, no: undefined },
52 | { key: 'value', tab: ['a', 'b'], bool: false },
53 | ),
54 | ).to.equal(false);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import MapboxMap from 'MapboxMap';
2 |
3 | export { default as mapboxgl } from 'Lib';
4 | export { default as Circle } from 'Circle';
5 | export { default as Helpers } from 'Helpers';
6 | export { default as Marker } from 'Marker';
7 | export { default as Popup } from 'Popup';
8 | export { default as Utils } from 'Utils';
9 | export { default as Diagnose } from 'Diagnose';
10 |
11 | export default MapboxMap;
12 |
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 |
3 | import { JSDOM } from 'jsdom';
4 | import { configure } from 'enzyme';
5 | import Adapter from 'enzyme-adapter-react-16';
6 |
7 | global.chai = require('chai');
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | global.expect = global.chai.expect;
12 | global.assert = global.chai.assert;
13 |
14 | global.document = new JSDOM('').window.document;
15 | global.window = document.defaultView;
16 | global.navigator = window.navigator;
17 |
--------------------------------------------------------------------------------
/tools/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -o nounset -o pipefail
4 |
5 | run_command() {
6 | if ! "${@}"; then
7 | printf "%bCommit Failed%b\n" "${RED}" "${RESET}"
8 | printf "%bPlease fix errors and try committing again.%b\n" "${RED}" "${RESET}"
9 |
10 | exit 1
11 | fi
12 | }
13 |
14 | get_files() {
15 | git diff --name-only --diff-filter=ACMR --cached -- "${@}" | tr '\n' ' ' | sed "s| *$||g"
16 | }
17 |
18 | get_diff() {
19 | git diff --name-only --diff-filter=ACMR
20 | }
21 |
22 | diff_check() {
23 | if ! [[ "${1}" == "$(get_diff)" ]]; then
24 | printf "%bPre-commit has changed files%b\n" "${RED}" "${RESET}"
25 | printf "%bConsider adding updated file with %bgit add -i \&\& git commit --amend%b\n" "${RED}" "${BLUE}" "${RESET}"
26 | fi
27 | }
28 |
29 | npm_check() {
30 | local PACKAGE_JSON
31 | PACKAGE_JSON=$(get_files "package.json")
32 |
33 | local JS_FILES
34 | JS_FILES=$(get_files \*.js{,x})
35 |
36 | if [[ -n "${PACKAGE_JSON:-}" ]] || [[ -n "${JS_FILES:-}" ]]; then
37 | printf "%bChecking node_modules installation%b\n" "${BLUE}" "${RESET}"
38 | run_command npm --silent install --no-audit
39 | run_command npm --silent run peers
40 | fi
41 |
42 | if [[ -n "${JS_FILES:-}" ]]; then
43 | printf "%bFormating javascript files%b\n" "${BLUE}" "${RESET}"
44 | run_command npm --silent run format:file "${JS_FILES}"
45 |
46 | printf "%bLinting javascript files%b\n" "${BLUE}" "${RESET}"
47 | run_command npm --silent run lint:file "${JS_FILES}"
48 |
49 | printf "%bBuilding dist%b\n" "${BLUE}" "${RESET}"
50 | run_command npm --silent build
51 | fi
52 | }
53 |
54 | main() {
55 |
56 | local YELLOW='\033[33m'
57 | local RED='\033[0;31m'
58 | local BLUE='\033[0;34m'
59 | local RESET='\033[0m'
60 |
61 | local INITIAL_DIFF
62 | INITIAL_DIFF=$(get_diff)
63 |
64 | npm_check
65 |
66 | diff_check "${INITIAL_DIFF}"
67 |
68 | exit 0
69 | }
70 |
71 | main
72 |
--------------------------------------------------------------------------------
/tools/install_hooks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -o nounset -o pipefail -o errexit
4 |
5 | install() {
6 | local SCRIPT_DIR
7 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8 |
9 | local GREEN='\033[0;32m'
10 | local YELLOW='\033[33m'
11 | local RESET='\033[0m'
12 |
13 | (
14 | cd "${SCRIPT_DIR}"
15 | if [[ "$(git rev-parse --is-inside-work-tree 2>&1)" != "true" ]]; then
16 | printf "%bNot inside a git work tree%b\n" "${YELLOW}" "${RESET}"
17 | return
18 | fi
19 |
20 | local HOOKS_SOURCE_DIR="${SCRIPT_DIR}/hooks"
21 | local HOOKS_TARGET_DIR
22 | HOOKS_TARGET_DIR="$(git rev-parse --show-toplevel)/.git/hooks"
23 |
24 | if ! [[ -d "${HOOKS_TARGET_DIR}" ]]; then
25 | mkdir -p "${HOOKS_TARGET_DIR}"
26 | return
27 | fi
28 |
29 | (
30 | cd "${HOOKS_SOURCE_DIR}"
31 |
32 | for file in *; do
33 | ln -s -f "${HOOKS_SOURCE_DIR}/${file}" "${HOOKS_TARGET_DIR}/${file}"
34 | printf "%b✔ %s hook installed %b\n" "${GREEN}" "${file}" "${RESET}"
35 | done
36 | )
37 | )
38 | }
39 |
40 | install
41 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const nodeExternals = require('webpack-node-externals');
3 |
4 | /**
5 | * Determine wether or not is the production build.
6 | * @type {Boolean}
7 | */
8 | const isBuild = process.env.npm_lifecycle_event === 'build';
9 |
10 | module.exports = {
11 | entry: './src/index.js',
12 |
13 | output: {
14 | path: path.resolve(__dirname, 'build'),
15 | filename: 'index.js',
16 | libraryTarget: 'commonjs2',
17 | },
18 |
19 | module: {
20 | noParse: /(mapbox-gl)\.js$/,
21 | rules: [
22 | {
23 | test: /\.jsx?$/,
24 | exclude: /node_modules/,
25 | use: { loader: 'babel-loader' },
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
30 | },
31 | ],
32 | },
33 |
34 | mode: isBuild ? 'production' : 'development',
35 |
36 | resolve: {
37 | extensions: ['.js', '.jsx'],
38 | modules: ['node_modules', './src'],
39 | },
40 |
41 | externals: [
42 | nodeExternals({
43 | allowlist: [/mapbox-gl\/dist\/mapbox-gl.css/],
44 | }),
45 | ],
46 | };
47 |
--------------------------------------------------------------------------------