├── .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 | [![Build Status](https://travis-ci.org/MeilleursAgents/react-mapbox-wrapper.svg?branch=master)](https://travis-ci.org/MeilleursAgents/react-mapbox-wrapper) 4 | [![npm version](https://badge.fury.io/js/react-mapbox-wrapper.svg)](https://badge.fury.io/js/react-mapbox-wrapper) 5 | [![codecov](https://codecov.io/gh/MeilleursAgents/react-mapbox-wrapper/branch/master/graph/badge.svg)](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 | ![](sample.png) 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 | 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 | Fork me on GitHub 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 | --------------------------------------------------------------------------------