├── .babelrc ├── .eslintrc ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── dependabot.yml │ ├── nodejs.yml │ └── size-limit.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── gallery.jpg ├── installation.md └── introduction.md ├── flow-typed └── npm │ ├── gl.js │ └── sinon.js ├── package.json ├── rollup.config.js ├── src ├── __mocks__ │ └── mapbox-gl.js ├── assets │ └── favicon.ico ├── components │ ├── AttributionControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── CustomLayer │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── FeatureState │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Filter │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── FullscreenControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── GeolocateControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Image │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── LanguageControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Layer │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── MapContext │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── MapGL │ │ ├── README.md │ │ ├── eventProps.d.ts │ │ ├── eventProps.js │ │ ├── events.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Marker │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── NavigationControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Popup │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── ScaleControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ ├── Source │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js │ └── TrafficControl │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.test.js ├── index.d.ts ├── index.js ├── setupTests.js └── utils │ ├── capitalizeFirstLetter.js │ ├── diff.js │ ├── diff.test.js │ ├── generateEventProps.js │ ├── isArraysEqual.js │ ├── isArraysEqual.test.js │ ├── isBrowser.js │ ├── isElectron.js │ ├── mapbox-gl.js │ ├── normalizeChildren.js │ ├── point.js │ ├── queryRenderedFeatures.js │ ├── queryRenderedFeatures.test.js │ ├── shallowCompareChildren.js │ ├── shallowCompareChildren.test.js │ ├── validateSource.js │ └── validateSource.test.js ├── styleguide.config.js ├── styleguide.setup.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "bugfixes": true }], 4 | "@babel/preset-react", 5 | "@babel/preset-flow" 6 | ] 7 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "root": true, 4 | "extends": [ 5 | "airbnb", 6 | "plugin:flowtype/recommended", 7 | "prettier" 8 | ], 9 | "plugins": ["flowtype", "import", "react"], 10 | "env": { 11 | "browser": true, 12 | "jest": true 13 | }, 14 | "rules": { 15 | "arrow-parens": [ 16 | "warn", 17 | "as-needed", 18 | { 19 | "requireForBlockBody": true 20 | } 21 | ], 22 | "comma-dangle": ["error", "never"], 23 | "max-len": [ 24 | "error", 25 | { "code": 80, "ignoreStrings": true, "ignoreUrls": true } 26 | ], 27 | "no-dupe-class-members": 0, 28 | "no-return-assign": 0, 29 | "no-underscore-dangle": ["error", { "allowAfterThis": true }], 30 | "react/default-props-match-prop-types": [ 31 | "error", 32 | { 33 | "allowRequiredDefaults": true 34 | } 35 | ], 36 | "react/destructuring-assignment": 0, 37 | "react/no-unused-class-component-methods": 0, 38 | "react/prop-types": [1, { "skipUndeclared": true }], 39 | "react/sort-comp": [ 40 | 1, 41 | { 42 | "order": [ 43 | "type-annotations", 44 | "static-methods", 45 | "lifecycle", 46 | "everything-else", 47 | "render" 48 | ] 49 | } 50 | ], 51 | "react/state-in-constructor": ["error", "never"], 52 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 53 | "react/require-default-props": 0, 54 | "react/static-property-placement": ["error", "static public field"], 55 | "react/jsx-fragments": ["error", "element"], 56 | "import/no-extraneous-dependencies": [ 57 | "error", 58 | { "devDependencies": true, "peerDependencies": true } 59 | ], 60 | "quotes": ["error", "single"] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/findup/test/fixture/.* 3 | .*/malformed_package_json/.* 4 | .*/jsonlint-lines-primitives/test/fails/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | ./flow-typed 10 | ./node_modules/mapbox-gl/flow-typed 11 | ./node_modules/mapbox-gl/dist/mapbox-gl.js.flow 12 | 13 | [declarations] 14 | /node_modules/mapbox-gl/.* 15 | 16 | [options] 17 | server.max_workers=4 18 | esproposal.class_static_fields=enable 19 | esproposal.class_instance_fields=enable 20 | 21 | [lints] 22 | all=warn 23 | -------------------------------------------------------------------------------- /.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 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.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/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: npm 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "02:00" 14 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: write-all 5 | 6 | jobs: 7 | dependabot: 8 | runs-on: ubuntu-latest 9 | if: ${{ github.actor == 'dependabot[bot]' }} 10 | steps: 11 | - name: Dependabot metadata 12 | id: metadata 13 | uses: dependabot/fetch-metadata@v1.6.0 14 | with: 15 | github-token: "${{ secrets.GITHUB_TOKEN }}" 16 | - name: Approve Dependabot PRs 17 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 18 | run: gh pr review --approve "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | - name: Enable auto-merge for Dependabot PRs 23 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 24 | run: gh pr merge --auto --squash "$PR_URL" 25 | env: 26 | PR_URL: ${{github.event.pull_request.html_url}} 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | yarn 22 | yarn lint 23 | yarn flow 24 | yarn test 25 | yarn test:coverage 26 | yarn build 27 | env: 28 | CI: true 29 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: "size" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | size: 10 | runs-on: ubuntu-latest 11 | env: 12 | CI_JOB_NUMBER: 1 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: andresz1/size-limit-action@v1 16 | with: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # docs 10 | /styleguide 11 | 12 | # testing 13 | /coverage 14 | /flow-coverage 15 | 16 | # misc 17 | .env 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .idea 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /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 hello@urbica.co. 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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Installing 4 | 5 | Clone or fork and clone repo and install dependencies 6 | 7 | ```shell 8 | git clone https://github.com/urbica/react-map-gl.git 9 | cd react-map-gl 10 | npm install 11 | ``` 12 | 13 | ## Running Styleguidist server 14 | 15 | Start `react-styleguidist` server 16 | 17 | ```shell 18 | MAPBOX_ACCESS_TOKEN= npm start 19 | ``` 20 | 21 | where `` is a valid Mapbox [access token](https://www.mapbox.com/help/define-access-token/). 22 | 23 | ## Running tests 24 | 25 | Run tests with 26 | 27 | ```shell 28 | npm test 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Urbica Design 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Urbica React Mapbox GL JS 2 | 3 | [![Node CI](https://github.com/urbica/react-map-gl/workflows/Node%20CI/badge.svg)](https://github.com/urbica/react-map-gl/actions) 4 | [![codecov](https://codecov.io/gh/urbica/react-map-gl/branch/main/graph/badge.svg)](https://codecov.io/gh/urbica/react-map-gl) 5 | [![npm](https://img.shields.io/npm/dt/@urbica/react-map-gl.svg?style=popout)](https://www.npmjs.com/package/@urbica/react-map-gl) 6 | [![npm](https://img.shields.io/npm/v/@urbica/react-map-gl.svg?style=popout)](https://www.npmjs.com/package/@urbica/react-map-gl) 7 | ![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/@urbica/react-map-gl.svg) 8 | 9 | React Component Library for [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js). Mapbox GL JS is a JavaScript library that renders interactive maps from vector tiles and Mapbox styles using WebGL. This project is intended to be as close as possible to the [Mapbox GL JS API](https://docs.mapbox.com/mapbox-gl-js/api/). 10 | 11 | This project is heavily inspired by [uber/react-map-gl](https://github.com/uber/react-map-gl). 12 | 13 | - [Installation](#installation) 14 | - [Components](#components) 15 | - [Usage](#usage) 16 | - [Static Map](#static-map) 17 | - [Interactive Map](#interactive-map) 18 | - [MapGL with Source and Layer](#mapgl-with-source-and-layer) 19 | - [MapGL with GeoJSON Source](#mapgl-with-geojson-source) 20 | - [Custom Layers support](#custom-layers-support) 21 | - [Documentation](#documentation) 22 | - [Changelog](#changelog) 23 | - [License](#license) 24 | - [Contributing](#contributing) 25 | - [Team](#team) 26 | 27 | ![Gallery](https://raw.githubusercontent.com/urbica/react-map-gl/main/docs/gallery.jpg) 28 | 29 | ## Installation 30 | 31 | ```shell 32 | npm install --save mapbox-gl @urbica/react-map-gl 33 | ``` 34 | 35 | ...or if you are using yarn: 36 | 37 | ```shell 38 | yarn add mapbox-gl @urbica/react-map-gl 39 | ``` 40 | 41 | ### Optional Dependencies 42 | 43 | If you want to use the `LanguageControl`: 44 | 45 | ```shell 46 | npm install --save @mapbox/mapbox-gl-language 47 | ``` 48 | 49 | ...or if you are using yarn: 50 | 51 | ```shell 52 | yarn add @mapbox/mapbox-gl-language 53 | ``` 54 | 55 | ## Components 56 | 57 | | Component | Description | 58 | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 59 | | [MapGL](src/components/MapGL) | Represents map on the page | 60 | | [MapContext](src/components/MapContext) | React Context API for the map instance | 61 | | [Source](src/components/Source) | [Sources](https://docs.mapbox.com/mapbox-gl-js/api/#sources) specify the geographic features to be rendered on the map | 62 | | [Layer](src/components/Layer) | [Layers](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) specify the `Sources` style | 63 | | [Filter](src/components/Filter) | Set filter to existing layer | 64 | | [CustomLayer](src/components/CustomLayer) | Allow a user to render directly into the map's GL context | 65 | | [Image](src/components/Image) | Adds an image to the map style | 66 | | [Popup](src/components/Popup) | React Component for [Mapbox GL JS Popup](https://docs.mapbox.com/mapbox-gl-js/api/#popup) | 67 | | [Marker](src/components/Marker) | React Component for [Mapbox GL JS Marker](https://docs.mapbox.com/mapbox-gl-js/api/#marker) | 68 | | [FeatureState](src/components/FeatureState) | Sets the state of a geographic feature rendered on the map | 69 | | [AttributionControl](src/components/AttributionControl) | Represents the map's attribution information | 70 | | [LanguageControl](src/components/LanguageControl) | Adds support for switching the language of the map style | 71 | | [FullscreenControl](src/components/FullscreenControl) | Contains a button for toggling the map in and out of fullscreen mode | 72 | | [GeolocateControl](src/components/GeolocateControl) | Geolocate the user and then track their current location on the map | 73 | | [NavigationControl](src/components/NavigationControl) | Contains zoom buttons and a compass | 74 | | [ScaleControl](src/components/ScaleControl) | Displays the ratio of a distance on the map to the corresponding distance on the ground | 75 | | [Cluster](https://github.com/urbica/react-map-gl-cluster) | Cluster [Markers](src/components/Marker) with [supercluster](https://github.com/mapbox/supercluster) | 76 | | [Draw](https://github.com/urbica/react-map-gl-draw) | Support for drawing and editing features | 77 | 78 | ## Usage 79 | 80 | To use any of Mapbox’s tools, APIs, or SDKs, you’ll need a Mapbox [access token](https://www.mapbox.com/help/define-access-token/). Mapbox uses access tokens to associate requests to API resources with your account. You can find all your access tokens, create new ones, or delete existing ones on your [API access tokens page](https://www.mapbox.com/studio/account/tokens/). 81 | 82 | See [**Documentation**](https://urbica.github.io/react-map-gl/) for more examples. 83 | 84 | ### Static Map 85 | 86 | By default, `MapGL` component renders in a static mode. That means that the user cannot interact with the map. 87 | 88 | ```jsx 89 | import React from 'react'; 90 | import MapGL from '@urbica/react-map-gl'; 91 | import 'mapbox-gl/dist/mapbox-gl.css'; 92 | 93 | ; 101 | ``` 102 | 103 | ### Interactive Map 104 | 105 | In most cases, you will want the user to interact with the map. To do this, you need to provide `onViewportChange` handler, that will update map viewport state. 106 | 107 | ```jsx 108 | import React, { useState } from 'react'; 109 | import MapGL from '@urbica/react-map-gl'; 110 | import 'mapbox-gl/dist/mapbox-gl.css'; 111 | 112 | const [viewport, setViewport] = useState({ 113 | latitude: 37.78, 114 | longitude: -122.41, 115 | zoom: 11 116 | }); 117 | 118 | ; 127 | ``` 128 | 129 | ### MapGL with Source and Layer 130 | 131 | [Sources](https://docs.mapbox.com/mapbox-gl-js/api/#sources) specify the geographic features to be rendered on the map. 132 | 133 | [Layers](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) specify the Sources styles. The type of layer is specified by the `"type"` property, and must be one of `background`, `fill`, `line`, `symbol`, `raster`, `circle`, `fill-extrusion`, `heatmap`, `hillshade`. 134 | 135 | Except for layers of the `background` type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled. 136 | 137 | ```jsx 138 | import React from 'react'; 139 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 140 | import 'mapbox-gl/dist/mapbox-gl.css'; 141 | 142 | 147 | 148 | 158 | ; 159 | ``` 160 | 161 | ### MapGL with GeoJSON Source 162 | 163 | To draw a GeoJSON on a map, add `Source` with the `type` property set to `geojson` and `data` property set to a URL or inline [GeoJSON](http://geojson.org/). 164 | 165 | ```jsx 166 | import React, { useState } from 'react'; 167 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 168 | import 'mapbox-gl/dist/mapbox-gl.css'; 169 | 170 | const [viewport, setViewport] = useState({ 171 | latitude: 37.830348, 172 | longitude: -122.486052, 173 | zoom: 15 174 | }); 175 | 176 | const data = { 177 | type: 'Feature', 178 | geometry: { 179 | type: 'LineString', 180 | coordinates: [ 181 | [-122.48369693756104, 37.83381888486939], 182 | [-122.48348236083984, 37.83317489144141], 183 | [-122.48339653015138, 37.83270036637107], 184 | [-122.48356819152832, 37.832056363179625], 185 | [-122.48404026031496, 37.83114119107971], 186 | [-122.48404026031496, 37.83049717427869], 187 | [-122.48348236083984, 37.829920943955045], 188 | [-122.48356819152832, 37.82954808664175], 189 | [-122.48507022857666, 37.82944639795659], 190 | [-122.48610019683838, 37.82880236636284], 191 | [-122.48695850372314, 37.82931081282506], 192 | [-122.48700141906738, 37.83080223556934], 193 | [-122.48751640319824, 37.83168351665737], 194 | [-122.48803138732912, 37.832158048267786], 195 | [-122.48888969421387, 37.83297152392784], 196 | [-122.48987674713133, 37.83263257682617], 197 | [-122.49043464660643, 37.832937629287755], 198 | [-122.49125003814696, 37.832429207817725], 199 | [-122.49163627624512, 37.832564787218985], 200 | [-122.49223709106445, 37.83337825839438], 201 | [-122.49378204345702, 37.83368330777276] 202 | ] 203 | } 204 | }; 205 | 206 | 213 | 214 | 227 | ; 228 | ``` 229 | 230 | ### Custom Layers support 231 | 232 | [Custom layers](https://docs.mapbox.com/mapbox-gl-js/api/#customlayerinterface) allow a user to render directly into the map's GL context using the map's camera. 233 | 234 | Here is an Uber [deck.gl](https://github.com/uber/deck.gl) usage example. 235 | 236 | ```jsx 237 | import React from 'react'; 238 | import MapGL, { CustomLayer } from '@urbica/react-map-gl'; 239 | import { MapboxLayer } from '@deck.gl/mapbox'; 240 | import { ScatterplotLayer } from '@deck.gl/layers'; 241 | import 'mapbox-gl/dist/mapbox-gl.css'; 242 | 243 | const myDeckLayer = new MapboxLayer({ 244 | id: 'my-scatterplot', 245 | type: ScatterplotLayer, 246 | data: [{ position: [-74.5, 40], size: 1000 }], 247 | getPosition: (d) => d.position, 248 | getRadius: (d) => d.size, 249 | getColor: [255, 0, 0] 250 | }); 251 | 252 | 260 | 261 | ; 262 | ``` 263 | 264 | ## Documentation 265 | 266 | Check out [documentation website](https://urbica.github.io/react-map-gl/). 267 | 268 | ## Changelog 269 | 270 | Check out [CHANGELOG.md](CHANGELOG.md) and [releases](https://github.com/urbica/react-map-gl/releases) page. 271 | 272 | ## License 273 | 274 | This project is licensed under the terms of the [MIT license](LICENSE). 275 | 276 | ## Contributing 277 | 278 | Clone and install dependencies 279 | 280 | ```shell 281 | git clone https://github.com/urbica/react-map-gl.git 282 | cd react-map-gl 283 | npm install 284 | ``` 285 | 286 | Start `react-styleguidist` server 287 | 288 | ```shell 289 | MAPBOX_ACCESS_TOKEN= npm start 290 | ``` 291 | 292 | where `` is a valid Mapbox [access token](https://www.mapbox.com/help/define-access-token/). 293 | 294 | Run tests with 295 | 296 | ```shell 297 | npm test 298 | ``` 299 | 300 | ## Team 301 | 302 | | [![Stepan Kuzmin](https://github.com/stepankuzmin.png?size=144)](https://github.com/stepankuzmin) | [![Artem Boyur](https://github.com/boyur.png?size=144)](https://github.com/boyur) | [![Andrey Bakhvalov](https://github.com/device25.png?size=144)](https://github.com/device25) | 303 | | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | 304 | | [Stepan Kuzmin](https://github.com/stepankuzmin) | [Artem Boyur](https://github.com/boyur) | [Andrey Bakhvalov](https://github.com/device25) | 305 | -------------------------------------------------------------------------------- /docs/gallery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urbica/react-map-gl/ac510e76c58f240243dea1c0a251e7c9ad62dc8d/docs/gallery.jpg -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | `@urbica/react-map-gl` requires `mapbox-gl` as peer dependency: 2 | 3 | ```shell 4 | npm install --save mapbox-gl @urbica/react-map-gl 5 | ``` 6 | 7 | ...or if you are using yarn: 8 | 9 | ```shell 10 | yarn add mapbox-gl @urbica/react-map-gl 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | React Component Library for [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js). Mapbox GL JS is a JavaScript library that renders interactive maps from vector tiles and Mapbox styles using WebGL. This project is intended to be as close as possible to the [Mapbox GL JS API](https://docs.mapbox.com/mapbox-gl-js/api/). 2 | 3 | To use any of Mapbox’s tools, APIs, or SDKs, you’ll need a Mapbox [access token](https://www.mapbox.com/help/define-access-token/). Mapbox uses access tokens to associate requests to API resources with your account. You can find all your access tokens, create new ones, or delete existing ones on your [API access tokens page](https://www.mapbox.com/studio/account/tokens/). 4 | 5 | This package is heavily inspired by [uber/react-map-gl](https://github.com/uber/react-map-gl). 6 | 7 | 8 | -------------------------------------------------------------------------------- /flow-typed/npm/gl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'gl' { 4 | declare export default Function; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/npm/sinon.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'sinon' { 4 | declare export default Function; 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@urbica/react-map-gl", 3 | "version": "1.16.2", 4 | "description": "React Component for Mapbox GL JS", 5 | "author": "Stepan Kuzmin (stepankuzmin.com)", 6 | "contributors": [ 7 | "Andrey Bakhvalov (https://github.com/device25)", 8 | "Artem Boyur (https://github.com/boyur)" 9 | ], 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/urbica/react-map-gl.git" 14 | }, 15 | "keywords": [ 16 | "mapbox-gl-js", 17 | "mapbox-gl", 18 | "mapbox", 19 | "react mapbox", 20 | "react component", 21 | "react-mapbox-gl", 22 | "react", 23 | "urbica" 24 | ], 25 | "types": "src/index.d.ts", 26 | "main": "dist/react-map-gl.cjs.js", 27 | "module": "dist/react-map-gl.esm.js", 28 | "files": [ 29 | "**/*.d.ts", 30 | "dist" 31 | ], 32 | "scripts": { 33 | "start": "styleguidist server", 34 | "lint": "eslint src", 35 | "lint:fix": "eslint src --fix", 36 | "test": "jest test", 37 | "test:coverage": "jest test --coverage && codecov", 38 | "flow": "flow check", 39 | "flow:coverage": "flow-coverage-report -i 'src/**/*.js' -x 'src/setupTests.js' -x 'src/__mocks__/*' -x 'src/**/*.test.js' -t html", 40 | "build": "rollup -c", 41 | "build:watch": "rollup -c -w", 42 | "format": "prettier-eslint --write $PWD\"src/**/*.js\"", 43 | "cz": "git-cz", 44 | "size": "size-limit", 45 | "release": "npm run build && standard-version", 46 | "prepublishOnly": "npm run build", 47 | "styleguide:build": "styleguidist build", 48 | "styleguide:deploy": "gh-pages -m 'auto commit [ci skip]' -d styleguide", 49 | "postpublish": "npm run styleguide:build && npm run styleguide:deploy", 50 | "prepare": "npm run build && husky install" 51 | }, 52 | "dependencies": {}, 53 | "peerDependencies": { 54 | "mapbox-gl": "^0.x || ^1.x || ^2.x", 55 | "react": "^16.x || ^17.x || ^18.x", 56 | "react-dom": "^16.x || ^17.x || ^18.x" 57 | }, 58 | "optionalDependencies": { 59 | "@mapbox/mapbox-gl-language": "^0.10.0", 60 | "@mapbox/mapbox-gl-traffic": "^1.0.2" 61 | }, 62 | "devDependencies": { 63 | "@babel/core": "^7.18.6", 64 | "@babel/eslint-parser": "^7.18.2", 65 | "@babel/preset-env": "^7.18.6", 66 | "@babel/preset-flow": "^7.18.6", 67 | "@babel/preset-react": "^7.18.6", 68 | "@deck.gl/core": "^8.8.3", 69 | "@deck.gl/layers": "^8.8.3", 70 | "@deck.gl/mapbox": "^8.8.3", 71 | "@luma.gl/core": "^8.5.14", 72 | "@mapbox/mapbox-gl-language": "^0.10.0", 73 | "@mapbox/mapbox-gl-traffic": "^1.0.2", 74 | "@rollup/plugin-babel": "^5.3.1", 75 | "@rollup/plugin-commonjs": "^22.0.1", 76 | "@rollup/plugin-node-resolve": "^13.3.0", 77 | "@size-limit/preset-big-lib": "^8.0.0", 78 | "@size-limit/time": "^8.0.0", 79 | "@turf/random": "^6.5.0", 80 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", 81 | "babel-loader": "^8.2.5", 82 | "codecov": "^3.8.3", 83 | "commitizen": "^4.2.4", 84 | "css-loader": "^6.7.1", 85 | "cz-conventional-changelog": "^3.3.0", 86 | "enzyme": "^3.11.0", 87 | "enzyme-adapter-react-16": "^1.15.6", 88 | "enzyme-to-json": "^3.6.2", 89 | "eslint": "^8.19.0", 90 | "eslint-config-airbnb": "^19.0.4", 91 | "eslint-config-prettier": "^8.5.0", 92 | "eslint-plugin-flowtype": "^8.0.3", 93 | "eslint-plugin-import": "^2.26.0", 94 | "eslint-plugin-jsx-a11y": "^6.6.0", 95 | "eslint-plugin-react": "^7.30.1", 96 | "flow-bin": "^0.129.0", 97 | "flow-coverage-report": "^0.8.0", 98 | "flow-remove-types": "^2.182.0", 99 | "gh-pages": "^4.0.0", 100 | "husky": "^8.0.1", 101 | "jest": "^26.6.3", 102 | "lint-staged": "^13.0.3", 103 | "mapbox-gl": "^1.13.2", 104 | "prettier": "^2.7.1", 105 | "prettier-eslint": "^15.0.1", 106 | "prettier-eslint-cli": "^7.0.0", 107 | "react": "^18.2.0", 108 | "react-dom": "^18.2.0", 109 | "react-styleguidist": "^11.2.0", 110 | "rollup": "^2.76.0", 111 | "rollup-plugin-terser": "^7.0.2", 112 | "size-limit": "^8.0.0", 113 | "standard-version": "^9.5.0", 114 | "style-loader": "^3.3.1", 115 | "webpack": "^5.73.0" 116 | }, 117 | "config": { 118 | "commitizen": { 119 | "path": "cz-conventional-changelog" 120 | } 121 | }, 122 | "husky": { 123 | "hooks": { 124 | "pre-commit": "lint-staged" 125 | } 126 | }, 127 | "jest": { 128 | "setupFiles": [ 129 | "/src/setupTests.js" 130 | ], 131 | "snapshotSerializers": [ 132 | "enzyme-to-json/serializer" 133 | ] 134 | }, 135 | "lint-staged": { 136 | "*.js": [ 137 | "prettier-eslint --write", 138 | "npm run lint", 139 | "jest --findRelatedTests" 140 | ] 141 | }, 142 | "size-limit": [ 143 | { 144 | "path": "dist/react-map-gl.cjs.js", 145 | "limit": "500 ms" 146 | }, 147 | { 148 | "path": "dist/react-map-gl.esm.js", 149 | "limit": "500 ms" 150 | } 151 | ], 152 | "browserslist": [ 153 | "defaults" 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { babel } from '@rollup/plugin-babel'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import pkg from './package.json'; 6 | 7 | export default { 8 | input: 'src/index.js', 9 | treeshake: true, 10 | output: [ 11 | { file: pkg.main, exports: 'named', sourcemap: true, format: 'cjs' }, 12 | { file: pkg.module, sourcemap: true, format: 'esm' } 13 | ], 14 | external: [ 15 | 'react', 16 | 'react-dom', 17 | 'mapbox-gl' 18 | ], 19 | plugins: [ 20 | nodeResolve(), 21 | babel({ babelHelpers: 'bundled' }), 22 | commonjs({ transformMixedEsModules: true }), 23 | terser() 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/__mocks__/mapbox-gl.js: -------------------------------------------------------------------------------- 1 | // LngLatBounds 2 | function LngLatBounds() {} 3 | 4 | LngLatBounds.prototype.toArray = () => [[-180, -90], [180, 90]]; 5 | 6 | // Map 7 | function Map() { 8 | this._sources = {}; 9 | this._images = {}; 10 | this._layers = []; 11 | this._controls = []; 12 | 13 | this.style = { 14 | sources: this._sources, 15 | layers: this._layers, 16 | sourceCaches: {} 17 | }; 18 | 19 | this.flyTo = jest.fn(); 20 | this.easeTo = jest.fn(); 21 | this.jumpTo = jest.fn(); 22 | 23 | this.getCanvas = jest.fn(() => ({ style: { cursor: 'default' } })); 24 | this.getCenter = jest.fn(() => ({ lat: 0, lng: 0 })); 25 | this.getBearing = jest.fn(() => 0); 26 | this.getPitch = jest.fn(() => 0); 27 | this.getZoom = jest.fn(() => 0); 28 | this.queryRenderedFeatures = jest.fn(() => []); 29 | this.setFeatureState = jest.fn(); 30 | this.removeFeatureState = jest.fn(); 31 | 32 | return this; 33 | } 34 | 35 | Map.prototype.once = function once(_, listener, fn) { 36 | const handler = typeof listener === 'function' ? listener : fn; 37 | handler({ target: this }); 38 | }; 39 | 40 | Map.prototype.on = function on(_, listener, fn) { 41 | const handler = typeof listener === 'function' ? listener : fn; 42 | handler({ target: this, originalEvent: true, point: { x: 0, y: 0 } }); 43 | }; 44 | 45 | Map.prototype.off = jest.fn(); 46 | 47 | Map.prototype.getStyle = function getStyle() { 48 | return this.style; 49 | }; 50 | 51 | Map.prototype.setStyle = jest.fn(); 52 | 53 | Map.prototype.addSource = function addSource(name, source) { 54 | this._sources[name] = source; 55 | this.style.sourceCaches[name] = { 56 | clearTiles: jest.fn() 57 | }; 58 | }; 59 | 60 | Map.prototype.getSource = function getSource(name) { 61 | if (!this._sources[name]) { 62 | return undefined; 63 | } 64 | 65 | const source = { 66 | setData: jest.fn(), 67 | load: jest.fn(), 68 | updateImage: jest.fn(), 69 | _tileJSONRequest: { 70 | cancel: jest.fn() 71 | }, 72 | ...this._sources[name] 73 | }; 74 | 75 | return source; 76 | }; 77 | 78 | Map.prototype.isSourceLoaded = function isSourceLoaded(name) { 79 | return !!this._sources[name]; 80 | }; 81 | 82 | Map.prototype.removeSource = function removeSource(name) { 83 | delete this._sources[name]; 84 | }; 85 | 86 | Map.prototype.addLayer = function addLayer(layer) { 87 | this._layers.push(layer); 88 | }; 89 | 90 | Map.prototype.getLayer = function getLayer(id) { 91 | const index = this._layers.findIndex(layer => id === layer.id); 92 | if (index === -1) { 93 | return undefined; 94 | } 95 | 96 | return this._layers[index]; 97 | }; 98 | 99 | Map.prototype.moveLayer = function moveLayer(id, before) { 100 | const index = this._layers.findIndex(layer => id === layer.id); 101 | const beforeIndex = this._layers.findIndex(layer => before === layer.id); 102 | if (!this._layers[index] || !this._layers[beforeIndex]) { 103 | throw new Error(); 104 | } 105 | 106 | const layer = this._layers[index]; 107 | this._layers.splice(index, 1); 108 | this._layers.splice(beforeIndex, 0, layer); 109 | }; 110 | 111 | Map.prototype.removeLayer = function removeLayer(id) { 112 | const index = this._layers.findIndex(layer => id === layer.id); 113 | if (!this._layers[index]) { 114 | throw new Error(); 115 | } 116 | 117 | this._layers.splice(index, 1); 118 | }; 119 | 120 | Map.prototype.loadImage = function loadImage(url, callback) { 121 | const data = new Uint8Array([]); 122 | callback(null, data); 123 | }; 124 | 125 | Map.prototype.addImage = function addImage(id, image) { 126 | this._images[id] = image; 127 | }; 128 | 129 | Map.prototype.updateImage = function updateImage(id, image) { 130 | this._images[id] = image; 131 | }; 132 | 133 | Map.prototype.hasImage = function hasImage(id) { 134 | return !!this._images[id]; 135 | }; 136 | 137 | Map.prototype.removeImage = function removeImage(id) { 138 | delete this._images[id]; 139 | }; 140 | 141 | Map.prototype.remove = jest.fn(); 142 | Map.prototype.addControl = function addControl(control) { 143 | control.onAdd(this); 144 | this._controls.push(control); 145 | 146 | return this; 147 | }; 148 | Map.prototype.removeControl = jest.fn(); 149 | Map.prototype.fire = jest.fn(); 150 | 151 | Map.prototype.setPaintProperty = jest.fn(); 152 | Map.prototype.setLayoutProperty = jest.fn(); 153 | Map.prototype.setFilter = jest.fn(); 154 | 155 | Map.prototype.getBounds = () => new LngLatBounds(); 156 | 157 | function Popup() { 158 | this.setLngLat = jest.fn(() => this); 159 | this.getLngLat = jest.fn(() => this); 160 | 161 | this.addTo = jest.fn((map) => { 162 | if (!map) { 163 | throw new Error(); 164 | } 165 | 166 | return this; 167 | }); 168 | 169 | this.setDOMContent = jest.fn(() => this); 170 | this.remove = jest.fn(); 171 | 172 | return this; 173 | } 174 | 175 | Popup.prototype.on = function on(listener, fn) { 176 | fn({ target: this }); 177 | }; 178 | 179 | function Marker() { 180 | this.setLngLat = jest.fn(() => this); 181 | this.getLngLat = jest.fn(() => this); 182 | 183 | this.addTo = jest.fn((map) => { 184 | if (!map) { 185 | throw new Error(); 186 | } 187 | 188 | return this; 189 | }); 190 | 191 | this.remove = jest.fn(); 192 | 193 | return this; 194 | } 195 | 196 | Marker.prototype.on = function on(listener, fn) { 197 | fn({ target: this }); 198 | }; 199 | 200 | function AttributionControl() { 201 | this.onAdd = jest.fn(); 202 | 203 | return this; 204 | } 205 | 206 | function FullscreenControl() { 207 | this.onAdd = jest.fn(); 208 | 209 | return this; 210 | } 211 | 212 | function GeolocateControl() { 213 | this.onAdd = jest.fn(); 214 | 215 | return this; 216 | } 217 | 218 | GeolocateControl.prototype.on = function on(listener, fn) { 219 | fn({ target: this }); 220 | }; 221 | 222 | function NavigationControl() { 223 | this.onAdd = jest.fn(); 224 | 225 | return this; 226 | } 227 | 228 | function ScaleControl() { 229 | this.onAdd = jest.fn(); 230 | 231 | return this; 232 | } 233 | 234 | function TrafficControl() { 235 | return this; 236 | } 237 | 238 | module.exports = { 239 | Map, 240 | Popup, 241 | Marker, 242 | AttributionControl, 243 | FullscreenControl, 244 | GeolocateControl, 245 | NavigationControl, 246 | ScaleControl, 247 | TrafficControl, 248 | supported: () => true 249 | }; 250 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urbica/react-map-gl/ac510e76c58f240243dea1c0a251e7c9ad62dc8d/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/components/AttributionControl/README.md: -------------------------------------------------------------------------------- 1 | An `AttributionControl` control presents the map's attribution information. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { AttributionControl } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 17 | 22 | ; 23 | ``` 24 | -------------------------------------------------------------------------------- /src/components/AttributionControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent, ReactNode } from "react"; 2 | import type { AttributionControl as MapboxAttributionControl } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /** 6 | * If `true` force a compact attribution that shows the full 7 | * attribution on mouse hover, or if false force the full attribution 8 | * control. The default is a responsive attribution that collapses when 9 | * the map is less than 640 pixels wide. 10 | */ 11 | compact?: boolean; 12 | 13 | /* String or strings to show in addition to any other attributions. */ 14 | customAttribution?: string | Array; 15 | 16 | /* A string representing the position of the control on the map. */ 17 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 18 | }; 19 | 20 | export default class AttributionControl extends PureComponent { 21 | getControl(): MapboxAttributionControl; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/AttributionControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxAttributionControl from 'mapbox-gl/src/ui/control/attribution_control'; 6 | 7 | import MapContext from '../MapContext'; 8 | import mapboxgl from '../../utils/mapbox-gl'; 9 | 10 | type Props = { 11 | /** 12 | * If `true` force a compact attribution that shows the full 13 | * attribution on mouse hover, or if false force the full attribution 14 | * control. The default is a responsive attribution that collapses when 15 | * the map is less than 640 pixels wide. 16 | */ 17 | compact: boolean, 18 | 19 | /* String or strings to show in addition to any other attributions. */ 20 | customAttribution: string | Array, 21 | 22 | /* A string representing the position of the control on the map. */ 23 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 24 | }; 25 | 26 | /** 27 | * An `AttributionControl` control presents the map's attribution information. 28 | */ 29 | class AttributionControl extends PureComponent { 30 | _map: MapboxMap; 31 | 32 | _control: MapboxAttributionControl; 33 | 34 | static defaultProps = { 35 | position: 'bottom-right' 36 | }; 37 | 38 | componentDidMount() { 39 | const map: MapboxMap = this._map; 40 | const { compact, customAttribution, position } = this.props; 41 | 42 | const control: MapboxAttributionControl = new mapboxgl.AttributionControl({ 43 | compact, 44 | customAttribution 45 | }); 46 | 47 | map.addControl(control, position); 48 | this._control = control; 49 | } 50 | 51 | componentWillUnmount() { 52 | if (!this._map || !this._map.getStyle()) { 53 | return; 54 | } 55 | 56 | this._map.removeControl(this._control); 57 | } 58 | 59 | getControl() { 60 | return this._control; 61 | } 62 | 63 | render() { 64 | return createElement(MapContext.Consumer, {}, (map) => { 65 | if (map) { 66 | this._map = map; 67 | } 68 | return null; 69 | }); 70 | } 71 | } 72 | 73 | export default AttributionControl; 74 | -------------------------------------------------------------------------------- /src/components/AttributionControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { AttributionControl } from '../..'; 6 | 7 | test('AttributionControl#render', () => { 8 | const wrapper = mount( 9 | 10 | 15 | 16 | ); 17 | 18 | const control = wrapper.find('AttributionControl'); 19 | expect(control.exists()).toBe(true); 20 | expect(control.instance().getControl()).toBeTruthy(); 21 | 22 | wrapper.unmount(); 23 | expect(wrapper.find('AttributionControl').exists()).toBe(false); 24 | }); 25 | 26 | test('throws', () => { 27 | console.error = jest.fn(); 28 | 29 | expect(() => mount()).toThrow(); 30 | expect(console.error).toHaveBeenCalled(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/CustomLayer/README.md: -------------------------------------------------------------------------------- 1 | [Custom layers](https://docs.mapbox.com/mapbox-gl-js/api/#customlayerinterface) allow a user to render directly into the map's GL context using the map's camera. 2 | 3 | Here is an Uber [deck.gl](https://github.com/uber/deck.gl) usage example. 4 | 5 | ```jsx 6 | import { MapboxLayer } from '@deck.gl/mapbox'; 7 | import { ArcLayer } from '@deck.gl/layers'; 8 | import MapGL, { CustomLayer } from '@urbica/react-map-gl'; 9 | import 'mapbox-gl/dist/mapbox-gl.css'; 10 | 11 | const myDeckLayer = new MapboxLayer({ 12 | id: 'deckgl-arc', 13 | type: ArcLayer, 14 | data: [ 15 | { source: [-122.3998664, 37.7883697], target: [-122.400068, 37.7900503] } 16 | ], 17 | getSourcePosition: (d) => d.source, 18 | getTargetPosition: (d) => d.target, 19 | getSourceColor: [255, 208, 0], 20 | getTargetColor: [0, 128, 255], 21 | getWidth: 8 22 | }); 23 | 24 | 34 | 35 | ; 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/CustomLayer/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent, createElement, ReactNode } from "react"; 2 | import type MapboxMap from "mapbox-gl/src/ui/map"; 3 | import type { CustomLayerInterface } from "mapbox-gl/src/style/style_layer/custom_style_layer"; 4 | import MapContext from "../MapContext"; 5 | 6 | type Props = { 7 | /** The id of an existing layer to insert the new layer before. */ 8 | before?: string; 9 | 10 | /** Mapbox GL Custom Layer instance */ 11 | layer: CustomLayerInterface; 12 | }; 13 | /** 14 | * Custom layers allow a user to render directly into the map's GL context 15 | * using the map's camera. 16 | */ 17 | 18 | export default class CustomLayer extends PureComponent { 19 | } 20 | -------------------------------------------------------------------------------- /src/components/CustomLayer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type { CustomLayerInterface } from 'mapbox-gl/src/style/style_layer/custom_style_layer'; 6 | 7 | import MapContext from '../MapContext'; 8 | 9 | type Props = { 10 | /** The id of an existing layer to insert the new layer before. */ 11 | before?: string, 12 | 13 | /** Mapbox GL Custom Layer instance */ 14 | layer: CustomLayerInterface 15 | }; 16 | 17 | /** 18 | * Custom layers allow a user to render directly into the map's GL context 19 | * using the map's camera. 20 | */ 21 | class CustomLayer extends PureComponent { 22 | _id: string; 23 | 24 | _map: MapboxMap; 25 | 26 | static displayName = 'CustomLayer'; 27 | 28 | componentDidMount() { 29 | const { layer, before } = this.props; 30 | 31 | if (before && this._map.getLayer(before)) { 32 | this._map.addLayer(layer, before); 33 | } else { 34 | this._map.addLayer(layer); 35 | } 36 | 37 | this._id = layer.id; 38 | } 39 | 40 | componentWillUnmount() { 41 | if (!this._map || !this._map.getStyle() || !this._map.getLayer(this._id)) { 42 | return; 43 | } 44 | 45 | this._map.removeLayer(this._id); 46 | } 47 | 48 | render() { 49 | return createElement(MapContext.Consumer, {}, (map) => { 50 | if (map) { 51 | this._map = map; 52 | } 53 | 54 | return null; 55 | }); 56 | } 57 | } 58 | 59 | export default CustomLayer; 60 | -------------------------------------------------------------------------------- /src/components/CustomLayer/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import { MapboxLayer } from '@deck.gl/mapbox'; 6 | import { ScatterplotLayer } from '@deck.gl/layers'; 7 | import MapGL, { Source, Layer, CustomLayer } from '../..'; 8 | 9 | test('render', () => { 10 | const myDeckLayer = new MapboxLayer({ 11 | id: 'my-scatterplot', 12 | type: ScatterplotLayer, 13 | data: [{ position: [-74.5, 40], size: 1000 }], 14 | getPosition: d => d.position, 15 | getRadius: d => d.size, 16 | getColor: [255, 0, 0] 17 | }); 18 | 19 | const wrapper = mount( 20 | 21 | 22 | 23 | ); 24 | 25 | expect(wrapper.find('CustomLayer').exists()).toBe(true); 26 | 27 | wrapper.unmount(); 28 | expect(wrapper.find('CustomLayer').exists()).toBe(false); 29 | }); 30 | 31 | test('before', () => { 32 | const data = { type: 'FeatureCollection', features: [] }; 33 | 34 | const DeckLayer1 = new MapboxLayer({ 35 | id: 'my-scatterplot-1', 36 | type: ScatterplotLayer, 37 | data: [{ position: [-74.5, 40], size: 1000 }], 38 | getPosition: d => d.position, 39 | getRadius: d => d.size, 40 | getColor: [255, 0, 0] 41 | }); 42 | 43 | const DeckLayer2 = new MapboxLayer({ 44 | id: 'my-scatterplot-2', 45 | type: ScatterplotLayer, 46 | data: [{ position: [-74.5, 40], size: 1000 }], 47 | getPosition: d => d.position, 48 | getRadius: d => d.size, 49 | getColor: [255, 0, 0] 50 | }); 51 | 52 | const wrapper = mount( 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | 61 | const CustomLayerWrapper = wrapper.find('CustomLayer'); 62 | const CustomLayerWrapper1 = CustomLayerWrapper.find({ layer: DeckLayer1 }); 63 | expect(CustomLayerWrapper1.props().before).toBe('test'); 64 | 65 | const CustomLayerWrapper2 = CustomLayerWrapper.find({ layer: DeckLayer2 }); 66 | expect(CustomLayerWrapper2.props().before).toBe('test'); 67 | }); 68 | 69 | test('throws', () => { 70 | console.error = jest.fn(); 71 | 72 | const myDeckLayer = new MapboxLayer({ 73 | id: 'my-scatterplot', 74 | type: ScatterplotLayer, 75 | data: [{ position: [-74.5, 40], size: 1000 }], 76 | getPosition: d => d.position, 77 | getRadius: d => d.size, 78 | getColor: [255, 0, 0] 79 | }); 80 | 81 | expect(() => mount()).toThrow(); 82 | expect(console.error).toHaveBeenCalled(); 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/FeatureState/README.md: -------------------------------------------------------------------------------- 1 | A `FeatureState` component sets the state of a feature. For example, you can use events and feature states to create a per feature hover effect. 2 | 3 | ```jsx 4 | import React, { useState } from 'react'; 5 | import MapGL, { Source, Layer, FeatureState } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | const [hoveredStateId, setHoveredStateId] = useState(null); 9 | 10 | const [viewport, setViewport] = useState({ 11 | latitude: 37.830348, 12 | longitude: -100.486052, 13 | zoom: 2 14 | }); 15 | 16 | const onHover = (event) => { 17 | if (event.features.length > 0) { 18 | const nextHoveredStateId = event.features[0].id; 19 | if (hoveredStateId !== nextHoveredStateId) { 20 | setHoveredStateId(nextHoveredStateId); 21 | } 22 | } 23 | }; 24 | 25 | const onLeave = () => { 26 | if (hoveredStateId) { 27 | setHoveredStateId(null); 28 | } 29 | }; 30 | 31 | 38 | 43 | 54 | {hoveredStateId && } 55 | ; 56 | ``` 57 | -------------------------------------------------------------------------------- /src/components/FeatureState/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | 3 | type Props = { 4 | /** Unique id of the feature. */ 5 | id: string | number; 6 | 7 | /** The Id of the vector source or GeoJSON source for the feature. */ 8 | source: string; 9 | 10 | /** For vector tile sources, the sourceLayer is required. */ 11 | sourceLayer: string; 12 | 13 | /** A set of key-value pairs. The values should be valid JSON types. */ 14 | state: Object; 15 | }; 16 | 17 | export default class FeatureState extends PureComponent {} 18 | -------------------------------------------------------------------------------- /src/components/FeatureState/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | 6 | import MapContext from '../MapContext'; 7 | 8 | type Props = { 9 | /** Unique id of the feature. */ 10 | id: string | number, 11 | 12 | /** The Id of the vector source or GeoJSON source for the feature. */ 13 | source: string, 14 | 15 | /** For vector tile sources, the sourceLayer is required. */ 16 | sourceLayer: string, 17 | 18 | /** A set of key-value pairs. The values should be valid JSON types. */ 19 | state: Object 20 | }; 21 | 22 | /** 23 | * A `FeatureState` component sets the state of a feature. 24 | */ 25 | class FeatureState extends PureComponent { 26 | _map: MapboxMap; 27 | 28 | componentDidMount() { 29 | const map: MapboxMap = this._map; 30 | const { id, source, sourceLayer, state } = this.props; 31 | map.setFeatureState({ id, source, sourceLayer }, state); 32 | } 33 | 34 | componentDidUpdate(prevProps: Props) { 35 | const map = this._map; 36 | const { id, source, sourceLayer, state } = this.props; 37 | 38 | if ( 39 | id !== prevProps.id || 40 | source !== prevProps.source || 41 | sourceLayer !== prevProps.sourceLayer || 42 | state !== prevProps.state 43 | ) { 44 | map.removeFeatureState({ 45 | id: prevProps.id, 46 | source: prevProps.source, 47 | sourceLayer: prevProps.sourceLayer 48 | }); 49 | 50 | map.setFeatureState({ id, source, sourceLayer }, state); 51 | } 52 | } 53 | 54 | componentWillUnmount() { 55 | if (!this._map || !this._map.getStyle()) { 56 | return; 57 | } 58 | 59 | const { id, source, sourceLayer } = this.props; 60 | this._map.removeFeatureState({ id, source, sourceLayer }); 61 | } 62 | 63 | render() { 64 | return createElement(MapContext.Consumer, {}, (map) => { 65 | if (map) { 66 | this._map = map; 67 | } 68 | 69 | return null; 70 | }); 71 | } 72 | } 73 | 74 | export default FeatureState; 75 | -------------------------------------------------------------------------------- /src/components/FeatureState/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Source, FeatureState } from '../..'; 6 | 7 | test('render', () => { 8 | const data = { type: 'FeatureCollection', features: [] }; 9 | 10 | const wrapper = mount( 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | expect(wrapper.find('FeatureState').exists()).toBe(true); 18 | 19 | wrapper.unmount(); 20 | expect(wrapper.find('FeatureState').exists()).toBe(false); 21 | }); 22 | 23 | test('update', () => { 24 | const data = { type: 'FeatureCollection', features: [] }; 25 | 26 | const wrapper = mount( 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | wrapper.setProps({ 34 | children: [ 35 | , 36 | 37 | ] 38 | }); 39 | }); 40 | 41 | test('throws', () => { 42 | console.error = jest.fn(); 43 | 44 | expect(() => 45 | mount() 46 | ).toThrow(); 47 | 48 | expect(console.error).toHaveBeenCalled(); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/Filter/README.md: -------------------------------------------------------------------------------- 1 | Set filter to target layer. Layer id can be an id of any layer defined in style or defined by `` component. 2 | 3 | ```js 4 | import React, { useState } from 'react'; 5 | import MapGL, { Source, Layer, Filter } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | const data = { 9 | type: 'FeatureCollection', 10 | features: [ 11 | { 12 | type: 'Feature', 13 | properties: { 14 | foo: 1 15 | }, 16 | geometry: { 17 | type: 'Point', 18 | coordinates: [-122.44400024414062, 37.82280243352756] 19 | } 20 | }, 21 | { 22 | type: 'Feature', 23 | properties: { 24 | foo: 2 25 | }, 26 | geometry: { 27 | type: 'Point', 28 | coordinates: [-122.37258911132812, 37.76610103745479] 29 | } 30 | }, 31 | { 32 | type: 'Feature', 33 | properties: { 34 | foo: 3 35 | }, 36 | geometry: { 37 | type: 'Point', 38 | coordinates: [-122.48451232910155, 37.75470124792827] 39 | } 40 | } 41 | ] 42 | }; 43 | 44 | const [filterValue, setFilterValue] = useState(1); 45 | 46 |
47 | 48 | 49 | 50 | 58 | 59 | 68 | 69 | 70 |
; 71 | ``` 72 | -------------------------------------------------------------------------------- /src/components/Filter/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { FilterSpecification } from "mapbox-gl/src/style-spec/types"; 2 | import { PureComponent } from "react"; 3 | 4 | type Props = { 5 | /** Mapbox GL Layer id */ 6 | layerId: string; 7 | /** 8 | * The filter, conforming to the Mapbox Style Specification's 9 | * filter definition. (see https://docs.mapbox.com/mapbox-gl-js/style-spec/#other-filter) 10 | * If null or undefined is provided, the function removes any existing filter 11 | * from the layer. 12 | * */ 13 | filter: FilterSpecification | null | undefined; 14 | 15 | /** 16 | * Whether to check if the filter conforms to the Mapbox GL 17 | * Style Specification. Disabling validation is a performance optimization 18 | * that should only be used if you have previously validated the values you 19 | * will be passing to this function. 20 | * */ 21 | validate?: boolean; 22 | }; 23 | 24 | export default class Filter extends PureComponent {} 25 | -------------------------------------------------------------------------------- /src/components/Filter/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type { FilterSpecification } from 'mapbox-gl/src/style-spec/types'; 6 | 7 | import MapContext from '../MapContext'; 8 | import isArraysEqual from '../../utils/isArraysEqual'; 9 | 10 | type Props = {| 11 | /** Mapbox GL Layer id */ 12 | layerId: string, 13 | /** 14 | * The filter, conforming to the Mapbox Style Specification's 15 | * filter definition. (see https://docs.mapbox.com/mapbox-gl-js/style-spec/#other-filter) 16 | * If null or undefined is provided, the function removes any existing filter 17 | * from the layer. 18 | * */ 19 | filter: FilterSpecification, 20 | 21 | /** 22 | * Whether to check if the filter conforms to the Mapbox GL 23 | * Style Specification. Disabling validation is a performance optimization 24 | * that should only be used if you have previously validated the values you 25 | * will be passing to this function. 26 | * */ 27 | validate?: boolean 28 | |}; 29 | 30 | class Filter extends PureComponent { 31 | _map: MapboxMap; 32 | 33 | static defaultProps = { 34 | validate: true 35 | }; 36 | 37 | componentDidMount() { 38 | this._setFilter(); 39 | } 40 | 41 | componentDidUpdate(prevProps: Props) { 42 | const prevFilter = prevProps.filter; 43 | const prevValidate = prevProps.validate; 44 | const { filter, validate } = this.props; 45 | 46 | const shouldUpdate = 47 | !isArraysEqual(prevFilter, filter) || prevValidate !== validate; 48 | 49 | if (shouldUpdate) { 50 | this._setFilter(); 51 | } 52 | } 53 | 54 | componentWillUnmount() { 55 | if (!this._map || !this._map.getStyle()) { 56 | return; 57 | } 58 | const { layerId } = this.props; 59 | const targetLayer = this._map.getLayer(layerId); 60 | 61 | if (targetLayer === undefined) { 62 | return; 63 | } 64 | 65 | this._map.setFilter(layerId, undefined); 66 | } 67 | 68 | _setFilter() { 69 | const { layerId, filter, validate } = this.props; 70 | const targetLayer = this._map.getLayer(layerId); 71 | 72 | if (targetLayer === undefined) { 73 | return; 74 | } 75 | 76 | if (!Array.isArray(filter)) { 77 | this._map.setFilter(layerId, undefined); 78 | } else { 79 | this._map.setFilter(layerId, filter, { validate }); 80 | } 81 | } 82 | 83 | render() { 84 | return createElement(MapContext.Consumer, {}, (map) => { 85 | if (map) { 86 | this._map = map; 87 | } 88 | 89 | return null; 90 | }); 91 | } 92 | } 93 | 94 | export default Filter; 95 | -------------------------------------------------------------------------------- /src/components/Filter/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import MapGL, { Filter, Layer } from '../..'; 4 | 5 | test('render', () => { 6 | const wrapper = mount( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | expect(wrapper.find('Filter').exists()).toBe(true); 17 | wrapper.setProps({ 18 | children: [ 19 | , 20 | , 21 | , 22 | 23 | ] 24 | }); 25 | 26 | wrapper.unmount(); 27 | expect(wrapper.find('Filter').exists()).toBe(false); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/FullscreenControl/README.md: -------------------------------------------------------------------------------- 1 | A `FullscreenControl` control contains a button for toggling the map in and out of fullscreen mode. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { FullscreenControl } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 16 | 17 | ; 18 | ``` 19 | -------------------------------------------------------------------------------- /src/components/FullscreenControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type { FullscreenControl as MapboxFullscreenControl } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /** 6 | * container is the compatible DOM element which should be 7 | * made full screen. By default, the map container element 8 | * will be made full screen. 9 | */ 10 | container?: string; 11 | 12 | /* A string representing the position of the control on the map. */ 13 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 14 | }; 15 | 16 | export default class FullscreenControl extends PureComponent { 17 | getControl(): MapboxFullscreenControl; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/FullscreenControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxFullscreenControl from 'mapbox-gl/src/ui/control/fullscreen_control'; 6 | 7 | import MapContext from '../MapContext'; 8 | import mapboxgl from '../../utils/mapbox-gl'; 9 | 10 | type Props = { 11 | /** 12 | * container is the compatible DOM element which should be 13 | * made full screen. By default, the map container element 14 | * will be made full screen. 15 | */ 16 | container: string, 17 | 18 | /* A string representing the position of the control on the map. */ 19 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 20 | }; 21 | 22 | /** 23 | * A `FullscreenControl` control contains a button for toggling the map in 24 | * and out of fullscreen mode. 25 | */ 26 | class FullscreenControl extends PureComponent { 27 | _map: MapboxMap; 28 | 29 | _control: MapboxFullscreenControl; 30 | 31 | static defaultProps = { 32 | position: 'top-right' 33 | }; 34 | 35 | componentDidMount() { 36 | const map: MapboxMap = this._map; 37 | const { container, position } = this.props; 38 | 39 | const control: MapboxFullscreenControl = new mapboxgl.FullscreenControl({ 40 | container 41 | }); 42 | 43 | map.addControl(control, position); 44 | this._control = control; 45 | } 46 | 47 | componentWillUnmount() { 48 | if (!this._map || !this._map.getStyle()) { 49 | return; 50 | } 51 | 52 | this._map.removeControl(this._control); 53 | } 54 | 55 | getControl() { 56 | return this._control; 57 | } 58 | 59 | render() { 60 | return createElement(MapContext.Consumer, {}, (map) => { 61 | if (map) { 62 | this._map = map; 63 | } 64 | return null; 65 | }); 66 | } 67 | } 68 | 69 | export default FullscreenControl; 70 | -------------------------------------------------------------------------------- /src/components/FullscreenControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { FullscreenControl } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | ); 13 | 14 | const control = wrapper.find('FullscreenControl'); 15 | expect(control.exists()).toBe(true); 16 | expect(control.instance().getControl()).toBeTruthy(); 17 | 18 | wrapper.unmount(); 19 | expect(wrapper.find('FullscreenControl').exists()).toBe(false); 20 | }); 21 | 22 | test('throws', () => { 23 | console.error = jest.fn(); 24 | 25 | expect(() => mount()).toThrow(); 26 | expect(console.error).toHaveBeenCalled(); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/GeolocateControl/README.md: -------------------------------------------------------------------------------- 1 | Geolocate the user and then track their current location on the map using the `GeolocateControl`. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { GeolocateControl } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 16 | 17 | ; 18 | ``` 19 | -------------------------------------------------------------------------------- /src/components/GeolocateControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type { GeolocateControl as MapboxGeolocateControl } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /* A Geolocation API PositionOptions object. */ 6 | positionOptions?: PositionOptions; 7 | 8 | /** 9 | * A `fitBounds` options object to use when the map is 10 | * panned and zoomed to the user's location. 11 | */ 12 | fitBoundsOptions?: Object; 13 | 14 | /** 15 | * If `true` the Geolocate Control becomes a toggle button and when active 16 | * the map will receive updates to the user's location as it changes. 17 | */ 18 | trackUserLocation?: boolean; 19 | 20 | /** 21 | * By default a dot will be shown on the map at the user's location. 22 | * Set to `false` to disable. 23 | */ 24 | showUserLocation?: boolean; 25 | 26 | /* A string representing the position of the control on the map. */ 27 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 28 | 29 | /** 30 | * Fired when the Geolocate Control changes to the background state. 31 | */ 32 | onTrackUserLocationEnd?: Function; 33 | 34 | /** 35 | * Fired when the Geolocate Control changes to the active lock state, 36 | */ 37 | onTrackUserLocationStart?: Function; 38 | 39 | /** 40 | * Fired on each Geolocation API position update which returned as an error. 41 | */ 42 | onError?: Function; 43 | 44 | /** 45 | * Fired on each Geolocation API position update which returned as success. 46 | */ 47 | onGeolocate?: Function; 48 | }; 49 | 50 | export default class GeolocateControl extends PureComponent { 51 | getControl(): MapboxGeolocateControl; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/GeolocateControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxGeolocateControl from 'mapbox-gl/src/ui/control/geolocate_control'; 6 | 7 | import MapContext from '../MapContext'; 8 | import mapboxgl from '../../utils/mapbox-gl'; 9 | 10 | type Props = { 11 | /* A Geolocation API PositionOptions object. */ 12 | positionOptions: PositionOptions, 13 | 14 | /** 15 | * A `fitBounds` options object to use when the map is 16 | * panned and zoomed to the user's location. 17 | */ 18 | fitBoundsOptions: Object, 19 | 20 | /** 21 | * If `true` the Geolocate Control becomes a toggle button and when active 22 | * the map will receive updates to the user's location as it changes. 23 | */ 24 | trackUserLocation: boolean, 25 | 26 | /** 27 | * By default a dot will be shown on the map at the user's location. 28 | * Set to `false` to disable. 29 | */ 30 | showUserLocation: boolean, 31 | 32 | /* A string representing the position of the control on the map. */ 33 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right', 34 | 35 | /** 36 | * Fired when the Geolocate Control changes to the background state. 37 | */ 38 | onTrackUserLocationEnd?: Function, 39 | 40 | /** 41 | * Fired when the Geolocate Control changes to the active lock state, 42 | */ 43 | onTrackUserLocationStart?: Function, 44 | 45 | /** 46 | * Fired on each Geolocation API position update which returned as an error. 47 | */ 48 | onError?: Function, 49 | 50 | /** 51 | * Fired on each Geolocation API position update which returned as success. 52 | */ 53 | onGeolocate?: Function 54 | }; 55 | 56 | /** 57 | * A `GeolocateControl` control provides a button that uses the browser's 58 | * geolocation API to locate the user on the map. 59 | */ 60 | class GeolocateControl extends PureComponent { 61 | _map: MapboxMap; 62 | 63 | _control: MapboxGeolocateControl; 64 | 65 | static defaultProps = { 66 | positionOptions: { enableHighAccuracy: false, timeout: 6000 }, 67 | fitBoundsOptions: { maxZoom: 15 }, 68 | trackUserLocation: false, 69 | showUserLocation: true 70 | }; 71 | 72 | componentDidMount() { 73 | const map: MapboxMap = this._map; 74 | const { 75 | positionOptions, 76 | fitBoundsOptions, 77 | trackUserLocation, 78 | showUserLocation, 79 | position, 80 | onTrackUserLocationEnd, 81 | onTrackUserLocationStart, 82 | onError, 83 | onGeolocate 84 | } = this.props; 85 | 86 | const control: MapboxGeolocateControl = new mapboxgl.GeolocateControl({ 87 | positionOptions, 88 | fitBoundsOptions, 89 | trackUserLocation, 90 | showUserLocation 91 | }); 92 | 93 | if (onTrackUserLocationEnd) { 94 | control.on('trackuserlocationend', onTrackUserLocationEnd); 95 | } 96 | 97 | if (onTrackUserLocationStart) { 98 | control.on('trackuserlocationstart', onTrackUserLocationStart); 99 | } 100 | 101 | if (onError) { 102 | control.on('error', onError); 103 | } 104 | 105 | if (onGeolocate) { 106 | control.on('geolocate', onGeolocate); 107 | } 108 | 109 | map.addControl(control, position); 110 | this._control = control; 111 | } 112 | 113 | componentWillUnmount() { 114 | if (!this._map || !this._map.getStyle()) { 115 | return; 116 | } 117 | 118 | this._map.removeControl(this._control); 119 | } 120 | 121 | getControl() { 122 | return this._control; 123 | } 124 | 125 | render() { 126 | return createElement(MapContext.Consumer, {}, (map) => { 127 | if (map) { 128 | this._map = map; 129 | } 130 | return null; 131 | }); 132 | } 133 | } 134 | 135 | export default GeolocateControl; 136 | -------------------------------------------------------------------------------- /src/components/GeolocateControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { GeolocateControl } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 17 | 18 | ); 19 | 20 | const control = wrapper.find('GeolocateControl'); 21 | expect(control.exists()).toBe(true); 22 | expect(control.instance().getControl()).toBeTruthy(); 23 | 24 | wrapper.unmount(); 25 | expect(wrapper.find('GeolocateControl').exists()).toBe(false); 26 | }); 27 | 28 | test('throws', () => { 29 | console.error = jest.fn(); 30 | 31 | expect(() => mount()).toThrow(); 32 | expect(console.error).toHaveBeenCalled(); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/Image/README.md: -------------------------------------------------------------------------------- 1 | Add an image to the style. This image can be used in `icon-image`, 2 | `background-pattern`, `fill-pattern`, and `line-pattern`. 3 | 4 | Mapbox examples: 5 | 6 | - [Add an icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image/) 7 | - [Add a generated icon to the map](https://www.mapbox.com/mapbox-gl-js/example/add-image-generated/) 8 | 9 | ```js 10 | import React, { useState } from 'react'; 11 | import MapGL, { Source, Layer, Image } from '@urbica/react-map-gl'; 12 | import 'mapbox-gl/dist/mapbox-gl.css'; 13 | 14 | const [viewport, setViewport] = useState({ 15 | latitude: 37.753574, 16 | longitude: -122.447303, 17 | zoom: 13 18 | }); 19 | 20 | const data = { 21 | type: 'FeatureCollection', 22 | features: [ 23 | { 24 | type: 'Feature', 25 | properties: {}, 26 | geometry: { 27 | type: 'Point', 28 | coordinates: [-122.45, 37.75] 29 | } 30 | } 31 | ] 32 | }; 33 | 34 | const imageURL = 35 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Cat_silhouette.svg/400px-Cat_silhouette.svg.png'; 36 | 37 | 44 | 45 | 46 | 55 | ; 56 | ``` 57 | -------------------------------------------------------------------------------- /src/components/Image/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { StyleImageInterface } from "mapbox-gl/src/style/style_image"; 2 | import { PureComponent } from "react"; 3 | 4 | type MapboxImage = 5 | | HTMLImageElement 6 | | ImageData 7 | | { width: number; height: number; data: Uint8Array | Uint8ClampedArray } 8 | | StyleImageInterface; 9 | 10 | type Props = { 11 | /** The ID of the image. */ 12 | id: string; 13 | 14 | /** 15 | * The image as an `HTMLImageElement`, `ImageData`, object with `width`, 16 | * `height`, and `data` properties with the same format as `ImageData` 17 | * or image url string 18 | * */ 19 | image: MapboxImage | string; 20 | 21 | /** The ratio of pixels in the image to physical pixels on the screen */ 22 | pixelRatio?: number; 23 | 24 | /** Whether the image should be interpreted as an SDF image */ 25 | sdf?: boolean; 26 | }; 27 | 28 | export default class Image extends PureComponent { 29 | constructor(props: Props); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Image/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type { StyleImageInterface } from 'mapbox-gl/src/style/style_image'; 6 | 7 | import MapContext from '../MapContext'; 8 | 9 | type MapboxImage = 10 | | HTMLImageElement 11 | | ImageData 12 | | { width: number, height: number, data: Uint8Array | Uint8ClampedArray } 13 | | StyleImageInterface; 14 | 15 | type Props = {| 16 | /** The ID of the image. */ 17 | id: string, 18 | 19 | /** 20 | * The image as an `HTMLImageElement`, `ImageData`, object with `width`, 21 | * `height`, and `data` properties with the same format as `ImageData` 22 | * or image url string 23 | * */ 24 | image: MapboxImage | string, 25 | 26 | /** The ratio of pixels in the image to physical pixels on the screen */ 27 | pixelRatio?: number, 28 | 29 | /** Whether the image should be interpreted as an SDF image */ 30 | sdf?: boolean 31 | |}; 32 | 33 | class Image extends PureComponent { 34 | _map: MapboxMap; 35 | 36 | _id: string; 37 | 38 | static defaultProps = { 39 | pixelRatio: 1, 40 | sdf: false 41 | }; 42 | 43 | constructor(props: Props) { 44 | super(props); 45 | this._id = props.id; 46 | } 47 | 48 | componentDidMount() { 49 | const { image, pixelRatio, sdf } = this.props; 50 | this._loadImage(image, data => 51 | this._map.addImage(this._id, data, { pixelRatio, sdf }) 52 | ); 53 | } 54 | 55 | componentDidUpdate(prevProps: Props) { 56 | const { id, image, pixelRatio, sdf } = this.props; 57 | 58 | if ( 59 | id !== prevProps.id || 60 | sdf !== prevProps.sdf || 61 | pixelRatio !== prevProps.pixelRatio 62 | ) { 63 | this._id = id; 64 | this._map.removeImage(prevProps.id); 65 | this._loadImage(image, data => 66 | this._map.addImage(this._id, data, { pixelRatio, sdf }) 67 | ); 68 | 69 | return; 70 | } 71 | 72 | if (image !== prevProps.image) { 73 | this._loadImage(image, data => this._map.updateImage(this._id, data)); 74 | } 75 | } 76 | 77 | componentWillUnmount() { 78 | if (!this._map || !this._map.getStyle() || !this._map.hasImage(this._id)) { 79 | return; 80 | } 81 | 82 | this._map.removeImage(this._id); 83 | } 84 | 85 | _loadImage = (image: MapboxImage | string, callback: MapboxImage => void) => { 86 | if (typeof image === 'string') { 87 | this._map.loadImage(image, (error, data) => { 88 | if (error) throw error; 89 | callback(data); 90 | }); 91 | 92 | return; 93 | } 94 | 95 | callback(image); 96 | }; 97 | 98 | render() { 99 | return createElement(MapContext.Consumer, {}, (map) => { 100 | if (map) { 101 | this._map = map; 102 | } 103 | 104 | return null; 105 | }); 106 | } 107 | } 108 | 109 | export default Image; 110 | -------------------------------------------------------------------------------- /src/components/Image/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Image } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | expect(wrapper.find('Image').exists()).toBe(true); 16 | expect(wrapper.find('Image')).toHaveLength(2); 17 | 18 | wrapper.unmount(); 19 | expect(wrapper.find('Image').exists()).toBe(false); 20 | }); 21 | 22 | test('update', () => { 23 | const wrapper = mount( 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | wrapper.setProps({ 31 | children: [ 32 | , 33 | 34 | ] 35 | }); 36 | 37 | wrapper.setProps({ 38 | children: [ 39 | , 40 | 41 | ] 42 | }); 43 | }); 44 | 45 | test('throws on loadImage', () => { 46 | console.error = jest.fn(); 47 | 48 | /* eslint-disable global-require */ 49 | const mapboxgl = require('../../__mocks__/mapbox-gl'); 50 | mapboxgl.Map.prototype.loadImage = function loadImage(url, callback) { 51 | callback(new Error()); 52 | }; 53 | 54 | jest.setMock('mapbox-gl', mapboxgl); 55 | 56 | expect(() => 57 | mount( 58 | 59 | 60 | 61 | ) 62 | ).toThrow(); 63 | 64 | expect(console.error).toHaveBeenCalled(); 65 | }); 66 | 67 | test('throws', () => { 68 | console.error = jest.fn(); 69 | 70 | expect(() => mount()).toThrow(); 71 | 72 | expect(console.error).toHaveBeenCalled(); 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/LanguageControl/README.md: -------------------------------------------------------------------------------- 1 | A `LanguageControl` adds support for switching the language of your map style. 2 | 3 | ⚠️ Requires the `@mapbox/mapbox-gl-language` package to be installed: 4 | 5 | ```shell 6 | npm install --save @mapbox/mapbox-gl-language 7 | ``` 8 | 9 | ...or 10 | 11 | ```shell 12 | yarn add @mapbox/mapbox-gl-language 13 | ``` 14 | 15 | ```js 16 | import React, { useState } from 'react'; 17 | import MapGL, { LanguageControl } from '@urbica/react-map-gl'; 18 | import 'mapbox-gl/dist/mapbox-gl.css'; 19 | 20 | const [language, setLanguage] = useState('fr'); 21 | 22 | <> 23 | 24 | 25 | 33 | 34 | 35 | ; 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/LanguageControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type MapboxLanguageControl from "@mapbox/mapbox-gl-language/index"; 3 | 4 | type Props = { 5 | /** List of supported languages */ 6 | supportedLanguages?: string[]; 7 | 8 | /** Custom style transformation to apply */ 9 | languageTransform?: Function; 10 | 11 | /** 12 | * RegExp to match if a text-field is a language field 13 | * (optional, default /^\{name/) 14 | */ 15 | languageField?: RegExp; 16 | 17 | /** Given a language choose the field in the vector tiles */ 18 | getLanguageField?: Function; 19 | 20 | /** Name of the source that contains the different languages. */ 21 | languageSource?: string; 22 | 23 | /** Name of the default language to initialize style after loading. */ 24 | defaultLanguage?: string; 25 | 26 | /** Name of the language to set */ 27 | language?: string; 28 | }; 29 | 30 | export default class LanguageControl extends PureComponent { 31 | getControl(): MapboxLanguageControl; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/LanguageControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxLanguageControl from '@mapbox/mapbox-gl-language/index'; 6 | 7 | import MapboxLanguage from '@mapbox/mapbox-gl-language'; 8 | import MapContext from '../MapContext'; 9 | 10 | type Props = { 11 | /** List of supported languages */ 12 | supportedLanguages?: string[], 13 | 14 | /** Custom style transformation to apply */ 15 | languageTransform?: Function, 16 | 17 | /** 18 | * RegExp to match if a text-field is a language field 19 | * (optional, default /^\{name/) 20 | */ 21 | languageField?: RegExp, 22 | 23 | /** Given a language choose the field in the vector tiles */ 24 | getLanguageField?: Function, 25 | 26 | /** Name of the source that contains the different languages. */ 27 | languageSource?: string, 28 | 29 | /** Name of the default language to initialize style after loading. */ 30 | defaultLanguage?: string, 31 | 32 | /** Name of the language to set */ 33 | language?: string 34 | }; 35 | 36 | /** 37 | * Adds support for switching the language of your map style. 38 | */ 39 | class LanguageControl extends PureComponent { 40 | _map: MapboxMap; 41 | 42 | _control: MapboxLanguageControl; 43 | 44 | static defaultProps = {}; 45 | 46 | componentDidMount() { 47 | const map: MapboxMap = this._map; 48 | const { 49 | supportedLanguages, 50 | languageTransform, 51 | languageField, 52 | getLanguageField, 53 | languageSource, 54 | defaultLanguage 55 | } = this.props; 56 | 57 | const control: MapboxLanguageControl = new MapboxLanguage({ 58 | supportedLanguages, 59 | languageTransform, 60 | languageField, 61 | getLanguageField, 62 | languageSource, 63 | defaultLanguage 64 | }); 65 | 66 | map.addControl(control); 67 | this._control = control; 68 | } 69 | 70 | componentDidUpdate(prevProps: Props) { 71 | if (prevProps.language !== this.props.language) { 72 | this._setLanguage(); 73 | } 74 | } 75 | 76 | componentWillUnmount() { 77 | if (!this._map || !this._map.getStyle()) { 78 | return; 79 | } 80 | 81 | this._map.removeControl(this._control); 82 | } 83 | 84 | _setLanguage = () => { 85 | const { language } = this.props; 86 | const map = this._map; 87 | const control = this._control; 88 | 89 | if (language) { 90 | map.setStyle(control.setLanguage(map.getStyle(), language)); 91 | } 92 | }; 93 | 94 | getControl() { 95 | return this._control; 96 | } 97 | 98 | render() { 99 | return createElement(MapContext.Consumer, {}, (map) => { 100 | if (map) { 101 | this._map = map; 102 | } 103 | return null; 104 | }); 105 | } 106 | } 107 | 108 | export default LanguageControl; 109 | -------------------------------------------------------------------------------- /src/components/LanguageControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { LanguageControl } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | ); 13 | 14 | const control = wrapper.find('LanguageControl'); 15 | expect(control.exists()).toBe(true); 16 | expect(control.instance().getControl()).toBeTruthy(); 17 | wrapper.setProps({ 18 | children: [] 19 | }); 20 | 21 | wrapper.unmount(); 22 | expect(wrapper.find('LanguageControl').exists()).toBe(false); 23 | }); 24 | 25 | test('throws', () => { 26 | console.error = jest.fn(); 27 | 28 | expect(() => mount()).toThrow(); 29 | expect(console.error).toHaveBeenCalled(); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Layer/README.md: -------------------------------------------------------------------------------- 1 | [Layers](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) specify the Sources styles. The type of layer is specified by the `'type'` property, and must be one of `background`, `fill`, `line`, `symbol`, `raster`, `circle`, `fill-extrusion`, `heatmap`, `hillshade`. 2 | 3 | Except for layers of the `background` type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled. 4 | 5 | ```jsx 6 | import React, { useState } from 'react'; 7 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 8 | import 'mapbox-gl/dist/mapbox-gl.css'; 9 | 10 | const [viewport, setViewport] = useState({ 11 | latitude: 37.78, 12 | longitude: -122.41, 13 | zoom: 11 14 | }); 15 | 16 | 23 | 24 | 34 | ; 35 | ``` 36 | 37 | ## Layer ordering 38 | 39 | You can add the `before` prop with the id of an existing layer to insert the new layer before. If this prop is omitted, the layer will be appended to the end of the layers array: 40 | 41 | ```xml 42 | 43 | ``` 44 | 45 | ```jsx 46 | import React, { useState } from 'react'; 47 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 48 | import 'mapbox-gl/dist/mapbox-gl.css'; 49 | 50 | const data = { 51 | red: { 52 | type: 'Feature', 53 | geometry: { 54 | type: 'Polygon', 55 | coordinates: [ 56 | [ 57 | [-122.4393653869629, 37.771664582389825], 58 | [-122.41936683654784, 37.771664582389825], 59 | [-122.41936683654784, 37.78679259356557], 60 | [-122.4393653869629, 37.78679259356557], 61 | [-122.4393653869629, 37.771664582389825] 62 | ] 63 | ] 64 | } 65 | }, 66 | green: { 67 | type: 'Feature', 68 | geometry: { 69 | type: 'Polygon', 70 | coordinates: [ 71 | [ 72 | [-122.43687629699707, 37.772750103327695], 73 | [-122.41722106933594, 37.772750103327695], 74 | [-122.41722106933594, 37.789031004883654], 75 | [-122.43687629699707, 37.789031004883654], 76 | [-122.43687629699707, 37.772750103327695] 77 | ] 78 | ] 79 | } 80 | }, 81 | blue: { 82 | type: 'Feature', 83 | geometry: { 84 | type: 'Polygon', 85 | coordinates: [ 86 | [ 87 | [-122.43498802185059, 37.7697648824009], 88 | [-122.4151611328125, 37.7697648824009], 89 | [-122.4151611328125, 37.78557161335955], 90 | [-122.43498802185059, 37.78557161335955], 91 | [-122.43498802185059, 37.7697648824009] 92 | ] 93 | ] 94 | } 95 | } 96 | }; 97 | 98 | const [beforeOrder, setBeforeOrder] = useState({ 99 | red: 'green', 100 | green: 'blue', 101 | blue: undefined 102 | }); 103 | 104 | const [viewport, setViewport] = useState({ 105 | latitude: 37.78, 106 | longitude: -122.41, 107 | zoom: 13 108 | }); 109 | 110 | const onChange = (layerId, event) => { 111 | const before = event.target.value || undefined; 112 | setBeforeOrder({ ...beforeOrder, [layerId]: before }); 113 | }; 114 | 115 | 116 | {Object.entries(beforeOrder).map(([layerId, before]) => ( 117 | 131 | ))} 132 | 139 | {Object.entries(beforeOrder).map(([layerId, before]) => ( 140 | 141 | 142 | 152 | 153 | ))} 154 | 155 | ; 156 | ``` 157 | -------------------------------------------------------------------------------- /src/components/Layer/index.d.ts: -------------------------------------------------------------------------------- 1 | import { MapMouseEvent } from "mapbox-gl"; 2 | import type GeoJSONFeature from "mapbox-gl/src/util/vectortile_to_geojson"; 3 | import { LayerSpecification } from "mapbox-gl/src/style-spec/types"; 4 | import { PureComponent } from "react"; 5 | 6 | type InteractionEvent = MapMouseEvent & { features?: GeoJSONFeature[] }; 7 | 8 | type Props = LayerSpecification & { 9 | /** Mapbox GL Layer id */ 10 | id: string; 11 | 12 | /** The id of an existing layer to insert the new layer before. */ 13 | before?: string; 14 | 15 | /** 16 | * Called when the layer is clicked. 17 | * @callback 18 | * @param {Object} event - The mouse event. 19 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 20 | * @param {Array} event.features - The features under the pointer, 21 | * using Mapbox's queryRenderedFeatures API: 22 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 23 | */ 24 | onClick?: (event: InteractionEvent) => void; 25 | 26 | /** 27 | * Called when the layer is hovered over. 28 | * @callback 29 | * @param {Object} event - The mouse event. 30 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 31 | * @param {Array} event.features - The features under the pointer, 32 | * using Mapbox's queryRenderedFeatures API: 33 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 34 | */ 35 | onHover?: (event: InteractionEvent) => void; 36 | 37 | /** 38 | * Called when the layer feature is entered. 39 | * @callback 40 | * @param {Object} event - The mouse event. 41 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 42 | * @param {Array} event.features - The features under the pointer, 43 | * using Mapbox's queryRenderedFeatures API: 44 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 45 | */ 46 | onEnter?: (event: InteractionEvent) => void; 47 | 48 | /** 49 | * Called when the layer feature is leaved. 50 | * @callback 51 | * @param {Object} event - The mouse event. 52 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 53 | * @param {Array} event.features - The features under the pointer, 54 | * using Mapbox's queryRenderedFeatures API: 55 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 56 | */ 57 | onLeave?: (event: InteractionEvent) => void; 58 | 59 | /** 60 | * Called when the layer is right-clicked. 61 | * @callback 62 | * @param {Object} event - The mouse event. 63 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 64 | * @param {Array} event.features - The features under the pointer, 65 | * using Mapbox's queryRenderedFeatures API: 66 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 67 | */ 68 | onContextMenu?: (event: InteractionEvent) => void; 69 | 70 | /** 71 | * Radius to detect features around a clicked/hovered point 72 | */ 73 | radius?: number; 74 | }; 75 | 76 | export default class Layer extends PureComponent {} 77 | -------------------------------------------------------------------------------- /src/components/Layer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type GeoJSONFeature from 'mapbox-gl/src/util/vectortile_to_geojson'; 6 | import type { LayerSpecification } from 'mapbox-gl/src/style-spec/types'; 7 | import type { MapMouseEvent, MapTouchEvent } from 'mapbox-gl/src/ui/events'; 8 | 9 | import MapContext from '../MapContext'; 10 | import diff from '../../utils/diff'; 11 | import queryRenderedFeatures from '../../utils/queryRenderedFeatures'; 12 | 13 | const eventListeners = [ 14 | ['onClick', 'click'], 15 | ['onHover', 'mousemove'], 16 | ['onEnter', 'mouseenter'], 17 | ['onLeave', 'mouseleave'], 18 | ['onContextMenu', 'contextmenu'] 19 | ]; 20 | 21 | type InteractionEvent = { ...MapMouseEvent, features?: GeoJSONFeature[] }; 22 | 23 | type Props = {| 24 | /** Mapbox GL Layer id */ 25 | id: string, 26 | 27 | /** The id of an existing layer to insert the new layer before. */ 28 | before?: string, 29 | 30 | /** 31 | * Called when the layer is clicked. 32 | * @callback 33 | * @param {Object} event - The mouse event. 34 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 35 | * @param {Array} event.features - The features under the pointer, 36 | * using Mapbox's queryRenderedFeatures API: 37 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 38 | */ 39 | onClick?: (event: InteractionEvent) => any, 40 | 41 | /** 42 | * Called when the layer is hovered over. 43 | * @callback 44 | * @param {Object} event - The mouse event. 45 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 46 | * @param {Array} event.features - The features under the pointer, 47 | * using Mapbox's queryRenderedFeatures API: 48 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 49 | */ 50 | onHover?: (event: InteractionEvent) => any, 51 | 52 | /** 53 | * Called when the layer feature is entered. 54 | * @callback 55 | * @param {Object} event - The mouse event. 56 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 57 | * @param {Array} event.features - The features under the pointer, 58 | * using Mapbox's queryRenderedFeatures API: 59 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 60 | */ 61 | onEnter?: (event: InteractionEvent) => any, 62 | 63 | /** 64 | * Called when the layer feature is leaved. 65 | * @callback 66 | * @param {Object} event - The mouse event. 67 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 68 | * @param {Array} event.features - The features under the pointer, 69 | * using Mapbox's queryRenderedFeatures API: 70 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 71 | */ 72 | onLeave?: (event: InteractionEvent) => any, 73 | 74 | /** 75 | * Called when the layer is right-clicked. 76 | * @callback 77 | * @param {Object} event - The mouse event. 78 | * @param {[Number, Number]} event.lngLat - The coordinates of the pointer 79 | * @param {Array} event.features - The features under the pointer, 80 | * using Mapbox's queryRenderedFeatures API: 81 | * https://www.mapbox.com/mapbox-gl-js/api/#Map#queryRenderedFeatures 82 | */ 83 | onContextMenu?: (event: InteractionEvent) => any, 84 | 85 | /** 86 | * Radius to detect features around a clicked/hovered point 87 | */ 88 | radius: number, 89 | 90 | ...LayerSpecification 91 | |}; 92 | 93 | class Layer extends PureComponent { 94 | $key: string; 95 | 96 | $value: any; 97 | 98 | _id: string; 99 | 100 | _map: MapboxMap; 101 | 102 | _onClick: (event: MapMouseEvent | MapTouchEvent) => void; 103 | 104 | _onHover: (event: MapMouseEvent) => void; 105 | 106 | _onEnter: (event: MapMouseEvent) => void; 107 | 108 | _onLeave: (event: MapMouseEvent) => void; 109 | 110 | _onContextMenu: (event: MapMouseEvent) => void; 111 | 112 | static displayName = 'Layer'; 113 | 114 | static defaultProps = { 115 | radius: 0 116 | }; 117 | 118 | constructor(props: Props) { 119 | super(props); 120 | this._id = props.id; 121 | } 122 | 123 | componentDidMount() { 124 | this._addLayer(this.props); 125 | } 126 | 127 | componentDidUpdate(prevProps: Props) { 128 | const map = this._map; 129 | const { id, before, onClick, ...layer } = this.props; 130 | 131 | if ( 132 | id !== prevProps.id || 133 | this.props.type !== prevProps.type || 134 | (prevProps.type !== 'background' && 135 | (this.props.source !== prevProps.source || 136 | // $FlowFixMe 137 | this.props['source-layer'] !== prevProps['source-layer'])) 138 | ) { 139 | this._id = id; 140 | this._map.removeLayer(prevProps.id); 141 | this._addLayer(this.props); 142 | return; 143 | } 144 | 145 | // flowlint sketchy-null-string:off 146 | if (before !== prevProps.before && before && map.getLayer(before)) { 147 | map.moveLayer(this._id, before); 148 | } 149 | // flowlint sketchy-null-string:warn 150 | 151 | if (layer.paint !== prevProps.paint) { 152 | diff(layer.paint, prevProps.paint).forEach(([key, value]) => { 153 | map.setPaintProperty(this._id, key, value); 154 | }); 155 | } 156 | 157 | if (layer.layout !== prevProps.layout) { 158 | diff(layer.layout, prevProps.layout).forEach(([key, value]) => { 159 | map.setLayoutProperty(this._id, key, value); 160 | }); 161 | } 162 | 163 | // $FlowFixMe 164 | if (layer.filter !== prevProps.filter) { 165 | if (!layer.filter) { 166 | map.setFilter(this._id, undefined); 167 | } else { 168 | map.setFilter(this._id, layer.filter); 169 | } 170 | } 171 | 172 | this._updateEventListeners(prevProps, this.props); 173 | } 174 | 175 | componentWillUnmount() { 176 | if (!this._map || !this._map.getStyle()) { 177 | return; 178 | } 179 | 180 | this._removeEventListeners(this.props); 181 | 182 | if (this._map.getLayer(this._id)) { 183 | this._map.removeLayer(this._id); 184 | } 185 | } 186 | 187 | _addLayer = (props: Props) => { 188 | const map = this._map; 189 | const { 190 | before, 191 | radius, 192 | onClick, 193 | onHover, 194 | onEnter, 195 | onLeave, 196 | onContextMenu, 197 | ...layer 198 | } = props; 199 | 200 | // flowlint sketchy-null-string:off 201 | if (before && map.getLayer(before)) { 202 | map.addLayer(layer, before); 203 | } else { 204 | map.addLayer(layer); 205 | } 206 | // flowlint sketchy-null-string:warn 207 | 208 | this._addEventListeners(props); 209 | }; 210 | 211 | _addEventListeners = (props: Props) => { 212 | eventListeners.forEach(([propName, eventName]) => { 213 | const handlerName = `_${propName}`; 214 | if (props[propName]) { 215 | this._map.on(eventName, this._id, this[handlerName]); 216 | } 217 | }); 218 | }; 219 | 220 | _updateEventListeners = (prevProps: Props, props: Props) => { 221 | eventListeners.forEach(([propName, eventName]) => { 222 | const handlerName = `_${propName}`; 223 | 224 | if (!props[propName] && prevProps[propName]) { 225 | this._map.off(eventName, this._id, this[handlerName]); 226 | } 227 | 228 | if (props[propName] && !prevProps[propName]) { 229 | this._map.on(eventName, this._id, this[handlerName]); 230 | } 231 | }); 232 | }; 233 | 234 | _removeEventListeners = (props: Props) => { 235 | eventListeners.forEach(([propName, eventName]) => { 236 | const handlerName = `_${propName}`; 237 | if (props[propName]) { 238 | this._map.off(eventName, this._id, this[handlerName]); 239 | } 240 | }); 241 | }; 242 | 243 | _onClick = (event: MapMouseEvent): void => { 244 | const position = [event.point.x, event.point.y]; 245 | const features = queryRenderedFeatures( 246 | this._map, 247 | this._id, 248 | position, 249 | this.props.radius 250 | ); 251 | 252 | // $FlowFixMe 253 | this.props.onClick({ ...event, features }); 254 | }; 255 | 256 | _onHover = (event: MapMouseEvent): void => { 257 | const position = [event.point.x, event.point.y]; 258 | const features = queryRenderedFeatures( 259 | this._map, 260 | this._id, 261 | position, 262 | this.props.radius 263 | ); 264 | 265 | // $FlowFixMe 266 | this.props.onHover({ ...event, features }); 267 | }; 268 | 269 | _onEnter = (event: MapMouseEvent): void => { 270 | const position = [event.point.x, event.point.y]; 271 | const features = queryRenderedFeatures( 272 | this._map, 273 | this._id, 274 | position, 275 | this.props.radius 276 | ); 277 | 278 | // $FlowFixMe 279 | this.props.onEnter({ ...event, features }); 280 | }; 281 | 282 | _onLeave = (event: MapMouseEvent) => { 283 | const position: [number, number] = [event.point.x, event.point.y]; 284 | const features = queryRenderedFeatures( 285 | this._map, 286 | this._id, 287 | position, 288 | this.props.radius 289 | ); 290 | 291 | // $FlowFixMe 292 | this.props.onLeave({ ...event, features }); 293 | }; 294 | 295 | _onContextMenu = (event: MapMouseEvent) => { 296 | const position: [number, number] = [event.point.x, event.point.y]; 297 | const features = queryRenderedFeatures( 298 | this._map, 299 | this._id, 300 | position, 301 | this.props.radius 302 | ); 303 | 304 | // $FlowFixMe 305 | this.props.onContextMenu({ ...event, features }); 306 | } 307 | 308 | render() { 309 | return createElement(MapContext.Consumer, {}, (map) => { 310 | if (map) { 311 | this._map = map; 312 | } 313 | 314 | return null; 315 | }); 316 | } 317 | } 318 | 319 | export default Layer; 320 | -------------------------------------------------------------------------------- /src/components/Layer/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Source, Layer } from '../..'; 6 | 7 | test('Layer#render', () => { 8 | const handler = jest.fn(); 9 | const data = { type: 'FeatureCollection', features: [] }; 10 | 11 | const wrapper = mount( 12 | 13 | 14 | 23 | 24 | ); 25 | 26 | expect(wrapper.find('Layer').exists()).toBe(true); 27 | expect(handler).toHaveBeenCalledTimes(4); 28 | 29 | wrapper.unmount(); 30 | expect(wrapper.find('Layer').exists()).toBe(false); 31 | }); 32 | 33 | test('Layer#unmount', () => { 34 | const handler = jest.fn(); 35 | const data = { type: 'FeatureCollection', features: [] }; 36 | 37 | const wrapper = mount( 38 | 39 | 40 | 49 | 50 | ); 51 | 52 | const map = wrapper.instance().getMap(); 53 | expect(map.getLayer('test')).toBeTruthy(); 54 | 55 | wrapper.setProps({ 56 | children: [] 57 | }); 58 | 59 | expect(map.getLayer('test')).toBeFalsy(); 60 | }); 61 | 62 | test('update id', () => { 63 | const data = { type: 'FeatureCollection', features: [] }; 64 | 65 | const wrapper = mount( 66 | 67 | 68 | 69 | 70 | ); 71 | 72 | const map = wrapper.instance().getMap(); 73 | expect(map.getLayer('test1')).toBeTruthy(); 74 | 75 | wrapper.setProps({ 76 | children: [ 77 | , 78 | 79 | ] 80 | }); 81 | 82 | expect(map.getLayer('test1')).toBeFalsy(); 83 | expect(map.getLayer('test2')).toBeTruthy(); 84 | }); 85 | 86 | test('update type', () => { 87 | const data = { type: 'FeatureCollection', features: [] }; 88 | 89 | const wrapper = mount( 90 | 91 | 92 | 93 | 94 | ); 95 | 96 | const map = wrapper.instance().getMap(); 97 | expect(map.getLayer('test').type).toEqual('circle'); 98 | 99 | wrapper.setProps({ 100 | children: [ 101 | , 102 | 103 | ] 104 | }); 105 | 106 | expect(map.getLayer('test').type).toEqual('line'); 107 | }); 108 | 109 | test('update source and source-layer', () => { 110 | const data = { type: 'FeatureCollection', features: [] }; 111 | 112 | const wrapper = mount( 113 | 114 | 115 | 116 | 117 | ); 118 | 119 | const map = wrapper.instance().getMap(); 120 | const layer1 = map.getLayer('test'); 121 | expect(layer1.source).toEqual('test1'); 122 | expect(layer1['source-layer']).toEqual('test1'); 123 | 124 | wrapper.setProps({ 125 | children: [ 126 | , 127 | 128 | ] 129 | }); 130 | 131 | const layer2 = map.getLayer('test'); 132 | expect(layer2.source).toEqual('test2'); 133 | expect(layer2['source-layer']).toEqual('test2'); 134 | }); 135 | 136 | test('before', () => { 137 | const data = { type: 'FeatureCollection', features: [] }; 138 | 139 | const wrapper = mount( 140 | 141 | 142 | 143 | 144 | 145 | 146 | ); 147 | 148 | expect(wrapper.find('Layer').exists()).toBe(true); 149 | expect(wrapper.find('Layer')).toHaveLength(3); 150 | 151 | wrapper.unmount(); 152 | expect(wrapper.find('Layer').exists()).toBe(false); 153 | }); 154 | 155 | test('update', () => { 156 | const data = { type: 'FeatureCollection', features: [] }; 157 | 158 | const wrapper = mount( 159 | 160 | 161 | 162 | 163 | 164 | ); 165 | 166 | wrapper.setProps({ 167 | children: [ 168 | , 169 | , 170 | 179 | ] 180 | }); 181 | 182 | wrapper.setProps({ 183 | children: [ 184 | , 185 | , 186 | 187 | ] 188 | }); 189 | }); 190 | 191 | test('handlers', () => { 192 | const data = { type: 'FeatureCollection', features: [] }; 193 | const handler = jest.fn(); 194 | 195 | const wrapper = mount( 196 | 197 | 198 | 199 | 200 | ); 201 | 202 | wrapper.setProps({ 203 | children: [ 204 | , 205 | 215 | ] 216 | }); 217 | 218 | expect(handler).toHaveBeenCalledTimes(5); 219 | 220 | wrapper.setProps({ 221 | children: [ 222 | , 223 | 224 | ] 225 | }); 226 | 227 | expect(handler).toHaveBeenCalledTimes(5); 228 | 229 | // wrapper.setProps({ children: [] }); 230 | }); 231 | 232 | test('throws', () => { 233 | console.error = jest.fn(); 234 | 235 | expect(() => 236 | mount() 237 | ).toThrow(); 238 | 239 | expect(console.error).toHaveBeenCalled(); 240 | }); 241 | -------------------------------------------------------------------------------- /src/components/MapContext/README.md: -------------------------------------------------------------------------------- 1 | `MapContext` provides [React Context API](https://reactjs.org/docs/context.html) for the Mapbox GL JS map instance. 2 | 3 | ### Using `MapContext` 4 | 5 | You can also use `MapContext.Consumer` to obtain Mapbox GL JS Map instance. 6 | 7 | ```jsx 8 | import React from 'react'; 9 | import MapGL, { MapContext } from '@urbica/react-map-gl'; 10 | import 'mapbox-gl/dist/mapbox-gl.css'; 11 | 12 | 20 | 21 | {map => { 22 | map.setPaintProperty('water', 'fill-color', '#fdbdba'); 23 | return; 24 | }} 25 | 26 | ; 27 | ``` 28 | -------------------------------------------------------------------------------- /src/components/MapContext/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "react"; 2 | import { Map } from "mapbox-gl"; 3 | 4 | declare const MapContext: Context; 5 | 6 | export default MapContext; 7 | -------------------------------------------------------------------------------- /src/components/MapContext/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createContext } from 'react'; 4 | import type { Context } from 'react'; 5 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 6 | 7 | const MapContext: Context = createContext(null); 8 | 9 | export default MapContext; 10 | -------------------------------------------------------------------------------- /src/components/MapContext/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | 6 | import MapGL, { MapContext } from '../..'; 7 | 8 | test('MapContext#render', () => { 9 | mount( 10 | 11 | 12 | {(map) => { 13 | expect(map).toBeTruthy(); 14 | return null; 15 | }} 16 | 17 | 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/MapGL/README.md: -------------------------------------------------------------------------------- 1 | The `MapGL` component represents map on the page. 2 | 3 | ### Static map 4 | 5 | By default, `MapGL` component renders in a static mode. That means that the user cannot interact with the map. 6 | 7 | ```jsx 8 | import React from 'react'; 9 | import MapGL from '@urbica/react-map-gl'; 10 | import 'mapbox-gl/dist/mapbox-gl.css'; 11 | 12 | ; 20 | ``` 21 | 22 | ### Interactive map 23 | 24 | In most cases, you will want the user to interact with the map. To do this, you need to provide `onViewportChange` handler, that will update map viewport state. 25 | 26 | ```jsx 27 | import React, { useState } from 'react'; 28 | import MapGL from '@urbica/react-map-gl'; 29 | import 'mapbox-gl/dist/mapbox-gl.css'; 30 | 31 | const [viewport, setViewport] = useState({ 32 | latitude: 37.78, 33 | longitude: -122.41, 34 | zoom: 11 35 | }); 36 | 37 | ; 46 | ``` 47 | 48 | ### Changing Map Style 49 | 50 | ```jsx 51 | import React, { useState } from 'react'; 52 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 53 | import 'mapbox-gl/dist/mapbox-gl.css'; 54 | 55 | const styles = { 56 | light: 'mapbox://styles/mapbox/light-v9', 57 | dark: 'mapbox://styles/mapbox/dark-v9' 58 | }; 59 | 60 | const [styleId, setStyleId] = useState('light'); 61 | 62 | const [viewport, setViewport] = useState({ 63 | latitude: 37.78, 64 | longitude: -122.41, 65 | zoom: 11 66 | }); 67 | 68 | 69 | 70 | 71 | 80 | ; 81 | ``` 82 | 83 | ### Using JSON Map Style 84 | 85 | ```jsx 86 | import React, { useState } from 'react'; 87 | import MapGL from '@urbica/react-map-gl'; 88 | import 'mapbox-gl/dist/mapbox-gl.css'; 89 | 90 | const [mapStyle, setMapStyle] = useState(null); 91 | 92 | const [viewport, setViewport] = useState({ 93 | latitude: 37.78, 94 | longitude: -122.41, 95 | zoom: 11 96 | }); 97 | 98 | const mapStyleURL = `https://api.mapbox.com/styles/v1/mapbox/light-v9?access_token=${MAPBOX_ACCESS_TOKEN}`; 99 | 100 | if (!mapStyle) { 101 | fetch(mapStyleURL) 102 | .then((response) => response.json()) 103 | .then((style) => setMapStyle(style)); 104 | } 105 | 106 | ; 113 | ``` 114 | 115 | ### Viewport Change Methods 116 | 117 | There are two props `viewportChangeMethod` and `viewportChangeOptions` that controls how `MapGL` component reacts to the new viewport props. 118 | 119 | You can find list of available `viewportChangeOptions` [here](https://docs.mapbox.com/mapbox-gl-js/api/#animationoptions). 120 | 121 | ```jsx 122 | import React, { useState } from 'react'; 123 | import MapGL from '@urbica/react-map-gl'; 124 | import 'mapbox-gl/dist/mapbox-gl.css'; 125 | 126 | const [viewportChangeMethod, setViewportChangeMethod] = useState('flyTo'); 127 | 128 | const [viewport, setViewport] = useState({ 129 | latitude: 37.78, 130 | longitude: -122.41, 131 | zoom: 11 132 | }); 133 | 134 | changeOptions = { 135 | duration: 1000 136 | }; 137 | 138 | const onChange = (event) => { 139 | setViewportChangeMethod(event.target.value); 140 | }; 141 | 142 | const onClick = (event) => { 143 | const { lngLat } = event; 144 | 145 | const newVewport = { 146 | ...viewport, 147 | latitude: lngLat.lat, 148 | longitude: lngLat.lng 149 | }; 150 | 151 | setViewport(newVewport); 152 | }; 153 | 154 | 155 | Select viewportChangeMethod and click on the map 156 | 161 | 171 | ; 172 | ``` 173 | 174 | ### Events 175 | 176 | `mapbox-gl` emit [events](https://www.mapbox.com/mapbox-gl-js/api/#events) in response to user interactions or changes in state. 177 | 178 | You can find full list of supported props in [eventProps](https://github.com/urbica/react-map-gl/blob/main/src/components/MapGL/eventProps.js). 179 | 180 | Here is an example for `onClick` prop. 181 | 182 | ```jsx 183 | import React, { useState } from 'react'; 184 | import MapGL from '@urbica/react-map-gl'; 185 | import 'mapbox-gl/dist/mapbox-gl.css'; 186 | 187 | const [viewport, setViewport] = useState({ 188 | latitude: 37.78, 189 | longitude: -122.41, 190 | zoom: 11 191 | }); 192 | 193 | const onClick = (event) => { 194 | const { lngLat } = event; 195 | 196 | const newVewport = { 197 | ...viewport, 198 | latitude: lngLat.lat, 199 | longitude: lngLat.lng 200 | }; 201 | 202 | setViewport(newVewport); 203 | }; 204 | 205 | ; 213 | ``` 214 | 215 | #### Layer-specific events 216 | 217 | You can also set layer-specific event-handlers passing prop in the form of the array where the first element is a layer id and the second element is an event handler: 218 | 219 | ```markup 220 | onClick={['national_park', onClick]} 221 | ``` 222 | 223 | ### Getting Mapbox GL JS Map Instance 224 | 225 | #### Using `MapContext` 226 | 227 | To access Mapbox GL JS Map instance you can use `MapContext.Consumer` component. 228 | 229 | ```jsx 230 | import React from 'react'; 231 | import MapGL, { MapContext } from '@urbica/react-map-gl'; 232 | import 'mapbox-gl/dist/mapbox-gl.css'; 233 | 234 | 242 | 243 | {(map) => { 244 | map.setPaintProperty('water', 'fill-color', '#fdbdba'); 245 | return; 246 | }} 247 | 248 | ; 249 | ``` 250 | 251 | #### Using Ref 252 | 253 | You can also call `getMap()` method on the `MapGL` [ref](https://reactjs.org/docs/refs-and-the-dom.html). 254 | 255 | ```jsx 256 | import React from 'react'; 257 | import MapGL from '@urbica/react-map-gl'; 258 | import 'mapbox-gl/dist/mapbox-gl.css'; 259 | 260 | class MyMapGL extends React.PureComponent { 261 | constructor(props) { 262 | super(props); 263 | this._map = React.createRef(); 264 | } 265 | 266 | componentDidMount() { 267 | const map = this._map.current.getMap(); 268 | map.once('load', () => { 269 | map.setPaintProperty('water', 'fill-color', '#db7093'); 270 | }); 271 | } 272 | 273 | render() { 274 | return ; 275 | } 276 | } 277 | 278 | ; 286 | ``` 287 | -------------------------------------------------------------------------------- /src/components/MapGL/eventProps.d.ts: -------------------------------------------------------------------------------- 1 | export type EventProps = { 2 | /** The resize event handler */ 3 | onResize?: Function; 4 | 5 | /** The remove event handler */ 6 | onRemove?: Function; 7 | 8 | /** The mousedown event handler */ 9 | onMousedown?: Function; 10 | 11 | /** The mouseup event handler */ 12 | onMouseup?: Function; 13 | 14 | /** The mouseover event handler */ 15 | onMouseover?: Function; 16 | 17 | /** The mousemove event handler */ 18 | onMousemove?: Function; 19 | 20 | /** The click event handler */ 21 | onClick?: Function; 22 | 23 | /** The dblclick event handler */ 24 | onDblclick?: Function; 25 | 26 | /** The mouseenter event handler */ 27 | onMouseenter?: Function; 28 | 29 | /** The mouseleave event handler */ 30 | onMouseleave?: Function; 31 | 32 | /** The mouseout event handler */ 33 | onMouseout?: Function; 34 | 35 | /** The contextmenu event handler */ 36 | onContextmenu?: Function; 37 | 38 | /** The touchstart event handler */ 39 | onTouchstart?: Function; 40 | 41 | /** The touchend event handler */ 42 | onTouchend?: Function; 43 | 44 | /** The touchmove event handler */ 45 | onTouchmove?: Function; 46 | 47 | /** The touchcancel event handler */ 48 | onTouchcancel?: Function; 49 | 50 | /** The movestart event handler */ 51 | onMovestart?: Function; 52 | 53 | /** The move event handler */ 54 | onMove?: Function; 55 | 56 | /** The moveend event handler */ 57 | onMoveend?: Function; 58 | 59 | /** The dragstart event handler */ 60 | onDragstart?: Function; 61 | 62 | /** The drag event handler */ 63 | onDrag?: Function; 64 | 65 | /** The dragend event handler */ 66 | onDragend?: Function; 67 | 68 | /** The zoomstart event handler */ 69 | onZoomstart?: Function; 70 | 71 | /** The zoom event handler */ 72 | onZoom?: Function; 73 | 74 | /** The zoomend event handler */ 75 | onZoomend?: Function; 76 | 77 | /** The rotatestart event handler */ 78 | onRotatestart?: Function; 79 | 80 | /** The rotate event handler */ 81 | onRotate?: Function; 82 | 83 | /** The rotateend event handler */ 84 | onRotateend?: Function; 85 | 86 | /** The pitchstart event handler */ 87 | onPitchstart?: Function; 88 | 89 | /** The pitch event handler */ 90 | onPitch?: Function; 91 | 92 | /** The pitchend event handler */ 93 | onPitchend?: Function; 94 | 95 | /** The boxzoomstart event handler */ 96 | onBoxzoomstart?: Function; 97 | 98 | /** The boxzoomend event handler */ 99 | onBoxzoomend?: Function; 100 | 101 | /** The boxzoomcancel event handler */ 102 | onBoxzoomcancel?: Function; 103 | 104 | /** The webglcontextlost event handler */ 105 | onWebglcontextlost?: Function; 106 | 107 | /** The webglcontextrestored event handler */ 108 | onWebglcontextrestored?: Function; 109 | 110 | /** The load event handler */ 111 | onLoad?: Function; 112 | 113 | /** The error event handler */ 114 | onError?: Function; 115 | 116 | /** The data event handler */ 117 | onData?: Function; 118 | 119 | /** The styledata event handler */ 120 | onStyledata?: Function; 121 | 122 | /** The sourcedata event handler */ 123 | onSourcedata?: Function; 124 | 125 | /** The dataloading event handler */ 126 | onDataloading?: Function; 127 | 128 | /** The styledataloading event handler */ 129 | onStyledataloading?: Function; 130 | 131 | /** The sourcedataloading event handler */ 132 | onSourcedataloading?: Function; 133 | }; 134 | -------------------------------------------------------------------------------- /src/components/MapGL/eventProps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type EventProps = { 4 | /** The resize event handler */ 5 | onResize?: Function, 6 | 7 | /** The remove event handler */ 8 | onRemove?: Function, 9 | 10 | /** The mousedown event handler */ 11 | onMousedown?: Function, 12 | 13 | /** The mouseup event handler */ 14 | onMouseup?: Function, 15 | 16 | /** The mouseover event handler */ 17 | onMouseover?: Function, 18 | 19 | /** The mousemove event handler */ 20 | onMousemove?: Function, 21 | 22 | /** The click event handler */ 23 | onClick?: Function, 24 | 25 | /** The dblclick event handler */ 26 | onDblclick?: Function, 27 | 28 | /** The mouseenter event handler */ 29 | onMouseenter?: Function, 30 | 31 | /** The mouseleave event handler */ 32 | onMouseleave?: Function, 33 | 34 | /** The mouseout event handler */ 35 | onMouseout?: Function, 36 | 37 | /** The contextmenu event handler */ 38 | onContextmenu?: Function, 39 | 40 | /** The touchstart event handler */ 41 | onTouchstart?: Function, 42 | 43 | /** The touchend event handler */ 44 | onTouchend?: Function, 45 | 46 | /** The touchmove event handler */ 47 | onTouchmove?: Function, 48 | 49 | /** The touchcancel event handler */ 50 | onTouchcancel?: Function, 51 | 52 | /** The movestart event handler */ 53 | onMovestart?: Function, 54 | 55 | /** The move event handler */ 56 | onMove?: Function, 57 | 58 | /** The moveend event handler */ 59 | onMoveend?: Function, 60 | 61 | /** The dragstart event handler */ 62 | onDragstart?: Function, 63 | 64 | /** The drag event handler */ 65 | onDrag?: Function, 66 | 67 | /** The dragend event handler */ 68 | onDragend?: Function, 69 | 70 | /** The zoomstart event handler */ 71 | onZoomstart?: Function, 72 | 73 | /** The zoom event handler */ 74 | onZoom?: Function, 75 | 76 | /** The zoomend event handler */ 77 | onZoomend?: Function, 78 | 79 | /** The rotatestart event handler */ 80 | onRotatestart?: Function, 81 | 82 | /** The rotate event handler */ 83 | onRotate?: Function, 84 | 85 | /** The rotateend event handler */ 86 | onRotateend?: Function, 87 | 88 | /** The pitchstart event handler */ 89 | onPitchstart?: Function, 90 | 91 | /** The pitch event handler */ 92 | onPitch?: Function, 93 | 94 | /** The pitchend event handler */ 95 | onPitchend?: Function, 96 | 97 | /** The boxzoomstart event handler */ 98 | onBoxzoomstart?: Function, 99 | 100 | /** The boxzoomend event handler */ 101 | onBoxzoomend?: Function, 102 | 103 | /** The boxzoomcancel event handler */ 104 | onBoxzoomcancel?: Function, 105 | 106 | /** The webglcontextlost event handler */ 107 | onWebglcontextlost?: Function, 108 | 109 | /** The webglcontextrestored event handler */ 110 | onWebglcontextrestored?: Function, 111 | 112 | /** The load event handler */ 113 | onLoad?: Function, 114 | 115 | /** The error event handler */ 116 | onError?: Function, 117 | 118 | /** The data event handler */ 119 | onData?: Function, 120 | 121 | /** The styledata event handler */ 122 | onStyledata?: Function, 123 | 124 | /** The sourcedata event handler */ 125 | onSourcedata?: Function, 126 | 127 | /** The dataloading event handler */ 128 | onDataloading?: Function, 129 | 130 | /** The styledataloading event handler */ 131 | onStyledataloading?: Function, 132 | 133 | /** The sourcedataloading event handler */ 134 | onSourcedataloading?: Function 135 | }; 136 | -------------------------------------------------------------------------------- /src/components/MapGL/events.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const events = [ 4 | 'resize', 5 | 'remove', 6 | 'mousedown', 7 | 'mouseup', 8 | 'mouseover', 9 | 'mousemove', 10 | 'click', 11 | 'dblclick', 12 | 'mouseenter', 13 | 'mouseleave', 14 | 'mouseout', 15 | 'contextmenu', 16 | 'touchstart', 17 | 'touchend', 18 | 'touchmove', 19 | 'touchcancel', 20 | 'movestart', 21 | 'move', 22 | 'moveend', 23 | 'dragstart', 24 | 'drag', 25 | 'dragend', 26 | 'zoomstart', 27 | 'zoom', 28 | 'zoomend', 29 | 'rotatestart', 30 | 'rotate', 31 | 'rotateend', 32 | 'pitchstart', 33 | 'pitch', 34 | 'pitchend', 35 | 'boxzoomstart', 36 | 'boxzoomend', 37 | 'boxzoomcancel', 38 | 'webglcontextlost', 39 | 'webglcontextrestored', 40 | 'load', 41 | 'error', 42 | 'data', 43 | 'styledata', 44 | 'sourcedata', 45 | 'dataloading', 46 | 'styledataloading', 47 | 'sourcedataloading' 48 | ]; 49 | 50 | module.exports = events; 51 | -------------------------------------------------------------------------------- /src/components/MapGL/index.d.ts: -------------------------------------------------------------------------------- 1 | import { StyleSpecification } from "mapbox-gl/src/style-spec/types"; 2 | import type { 3 | Map, 4 | MapMouseEvent, 5 | MapTouchEvent, 6 | LngLatBoundsLike, 7 | AnimationOptions, 8 | } from "mapbox-gl"; 9 | import type { PureComponent, ReactNode } from "react"; 10 | import { EventProps } from "./eventProps"; 11 | 12 | export type Viewport = { 13 | latitude: number; 14 | longitude: number; 15 | zoom: number; 16 | pitch?: number; 17 | bearing?: number; 18 | }; 19 | 20 | export const jumpTo = "jumpTo"; 21 | export const easeTo = "easeTo"; 22 | export const flyTo = "flyTo"; 23 | 24 | export type ViewportChangeMethod = "jumpTo" | "easeTo" | "flyTo"; 25 | export type ViewportChangeEvent = MapMouseEvent | MapTouchEvent; 26 | 27 | type Props = EventProps & { 28 | /** container className */ 29 | className?: string; 30 | 31 | /** container style */ 32 | style?: { 33 | [CSSProperty: string]: any; 34 | }; 35 | 36 | /** 37 | * The Mapbox style. A string url or a Mapbox GL style object. 38 | */ 39 | mapStyle: string | StyleSpecification; 40 | 41 | /** MapGL children as Sources, Layers, Controls, and custom Components */ 42 | children?: ReactNode; 43 | 44 | /** 45 | * Mapbox API access token for mapbox-gl-js. 46 | * Required when using Mapbox vector tiles/styles. 47 | */ 48 | accessToken?: string; 49 | 50 | /** The longitude of the center of the map. */ 51 | longitude: number; 52 | 53 | /** The latitude of the center of the map. */ 54 | latitude: number; 55 | 56 | /** The tile zoom level of the map. */ 57 | zoom: number; 58 | 59 | /** Specify the bearing of the viewport */ 60 | bearing?: number; 61 | 62 | /** Specify the pitch of the viewport */ 63 | pitch?: number; 64 | 65 | /** The minimum zoom level of the map (0-22). */ 66 | minZoom?: number; 67 | 68 | /** The maximum zoom level of the map (0-22). */ 69 | maxZoom?: number; 70 | 71 | /** 72 | * If `true`, the map's position (zoom, center latitude, 73 | * center longitude, bearing, and pitch) will be synced 74 | * with the hash fragment of the page's URL. For example, 75 | * http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60. 76 | */ 77 | hash?: boolean; 78 | 79 | /** 80 | * The threshold, measured in degrees, that determines when 81 | * the map's bearing (rotation) will snap to north. For 82 | * example, with a bearingSnap of 7, if the user rotates the 83 | * map within 7 degrees of north, the map will automatically 84 | * snap to exact north. 85 | */ 86 | bearingSnap?: number; 87 | 88 | /** 89 | * If `false`, the map's pitch (tilt) control with "drag to 90 | * rotate" interaction will be disabled. 91 | */ 92 | pitchWithRotate?: boolean; 93 | 94 | /** If `true`, an AttributionControl will be added to the map. */ 95 | attributionControl?: boolean; 96 | 97 | /* A string representing the position of the Mapbox wordmark on the map. */ 98 | logoPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 99 | 100 | /** 101 | * If `true`, map creation will fail if the performance of Mapbox 102 | * GL JS would be dramatically worse than expected (i.e. a software 103 | * renderer would be used). 104 | */ 105 | failIfMajorPerformanceCaveat?: boolean; 106 | 107 | /** 108 | * Mapbox WebGL context creation option. 109 | * Useful when you want to export the canvas as a PNG. 110 | */ 111 | preserveDrawingBuffer?: boolean; 112 | 113 | /** 114 | * If `true`, the gl context will be created with MSAA antialiasing, 115 | * which can be useful for antialiasing custom layers. this is `false` 116 | * by default as a performance optimization. 117 | */ 118 | antialias?: boolean; 119 | 120 | /** 121 | * If `false`, the map won't attempt to re-request tiles once they 122 | * expire per their HTTP `cacheControl`/`expires` headers. 123 | */ 124 | refreshExpiredTiles?: boolean; 125 | 126 | /** If set, the map will be constrained to the given bounds. */ 127 | maxBounds?: LngLatBoundsLike; 128 | 129 | /** 130 | * The initial bounds of the map. If bounds is specified, 131 | * it overrides center and zoom constructor options. 132 | * */ 133 | bounds?: LngLatBoundsLike; 134 | 135 | /** If `true`, the "scroll to zoom" interaction is enabled. */ 136 | scrollZoom?: boolean | Object; 137 | 138 | /** If `true`, the "box zoom" interaction is enabled */ 139 | boxZoom?: boolean; 140 | 141 | /** If `true`, the "drag to rotate" interaction is enabled */ 142 | dragRotate?: boolean; 143 | 144 | /** If `true`, the "drag to pan" interaction is enabled */ 145 | dragPan?: boolean; 146 | 147 | /** If `true`, keyboard shortcuts are enabled */ 148 | keyboard?: boolean; 149 | 150 | /** If `true`, the "double click to zoom" interaction is enabled */ 151 | doubleClickZoom?: boolean; 152 | 153 | /** 154 | * If `true`, the map will automatically resize 155 | * when the browser window resizes. 156 | */ 157 | trackResize?: boolean; 158 | 159 | /** 160 | * The maximum number of tiles stored in the tile cache for a given 161 | * source. If omitted, the cache will be dynamically sized based on 162 | * the current viewport. 163 | */ 164 | maxTileCacheSize?: number; 165 | 166 | /** 167 | * If `true`, multiple copies of the world 168 | * will be rendered, when zoomed out 169 | */ 170 | renderWorldCopies?: boolean; 171 | 172 | /** 173 | * If specified, defines a CSS font-family for locally overriding 174 | * generation of glyphs in the 'CJK Unified Ideographs' and 175 | * 'Hangul Syllables' ranges. In these ranges, font settings from the 176 | * map's style will be ignored, except for font-weight keywords 177 | * (light/regular/medium/bold). The purpose of this option is to avoid 178 | * bandwidth-intensive glyph server requests. 179 | * (see https://www.mapbox.com/mapbox-gl-js/example/local-ideographs ) 180 | */ 181 | localIdeographFontFamily?: boolean; 182 | 183 | /** 184 | * A callback run before the Map makes a request for an external URL. 185 | * The callback can be used to modify the url, set headers, or set the 186 | * credentials property for cross-origin requests. Expected to return 187 | * an object with a url property and optionally headers and 188 | * credentials properties. 189 | */ 190 | transformRequest?: ( 191 | url: string, 192 | resourceType: string 193 | ) => { url: string; headers?: Object; credentials?: string }; 194 | 195 | /** 196 | * If true, Resource Timing API information will be collected for 197 | * requests made by GeoJSON and Vector Tile web workers (this information 198 | * is normally inaccessible from the main Javascript thread). Information 199 | * will be returned in a resourceTiming property of relevant data events. 200 | */ 201 | collectResourceTiming?: boolean; 202 | 203 | /** 204 | * Controls the duration of the fade-in/fade-out animation for label 205 | * collisions, in milliseconds. This setting affects all symbol layers. 206 | * This setting does not affect the duration of runtime styling transitions 207 | * or raster tile cross-fading. 208 | */ 209 | fadeDuration?: number; 210 | 211 | /** 212 | * If `true`, symbols from multiple sources can collide with each 213 | * other during collision detection. If `false`, collision detection 214 | * is run separately for the symbols in each source. 215 | */ 216 | crossSourceCollisions?: boolean; 217 | 218 | /** 219 | * A patch to apply to the default localization table for UI strings, 220 | * e.g. control tooltips. The `locale` object maps namespaced UI string IDs 221 | * to translated strings in the target language; 222 | * see `src/ui/default_locale.js` for an example with all supported 223 | * string IDs. The object may specify all UI strings (thereby adding support 224 | * for a new translation) or only a subset of strings (thereby patching 225 | * the default translation table). 226 | */ 227 | locale?: string; 228 | 229 | /** 230 | * `onViewportChange` callback is fired when the user interacted with the 231 | * map. The object passed to the callback contains viewport properties 232 | * such as `longitude`, `latitude`, `zoom` etc. 233 | */ 234 | onViewportChange?: (viewport: Viewport) => void; 235 | 236 | /** 237 | * Map method that will be called after new viewport props are received. 238 | */ 239 | viewportChangeMethod?: ViewportChangeMethod; 240 | 241 | /** 242 | * Options common to map movement methods that involve animation, 243 | * controlling the duration and easing function of the animation. 244 | * This options will be passed to the `viewportChangeMethod` call. 245 | * (see https://docs.mapbox.com/mapbox-gl-js/api/#animationoptions) 246 | */ 247 | viewportChangeOptions?: AnimationOptions; 248 | 249 | /** The onLoad callback for the map */ 250 | onLoad?: Function; 251 | 252 | /** Map cursor style as CSS value */ 253 | cursorStyle?: string; 254 | 255 | /** 256 | * Sets a Boolean indicating whether the map will render an outline around 257 | * each tile and the tile ID. These tile boundaries are useful for debugging. 258 | * */ 259 | showTileBoundaries?: boolean; 260 | }; 261 | 262 | type State = { 263 | loaded: boolean; 264 | }; 265 | 266 | export default class MapGL extends PureComponent { 267 | constructor(props: Props); 268 | 269 | state: State; 270 | 271 | getMap(): Map; 272 | } 273 | -------------------------------------------------------------------------------- /src/components/Marker/README.md: -------------------------------------------------------------------------------- 1 | React Component for [Mapbox GL JS Marker](https://docs.mapbox.com/mapbox-gl-js/api/#marker). 2 | 3 | ```jsx 4 | import React, { useState } from 'react'; 5 | import MapGL, { Marker } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | const [position, setPosition] = useState({ 9 | longitude: 0, 10 | latitude: 0 11 | }); 12 | 13 | const style = { 14 | padding: '10px', 15 | color: '#fff', 16 | cursor: 'pointer', 17 | background: '#1978c8', 18 | borderRadius: '6px' 19 | }; 20 | 21 | const onDragEnd = (lngLat) => { 22 | setPosition({ longitude: lngLat.lng, latitude: lngLat.lat }); 23 | }; 24 | 25 | 33 | 39 |
Hi there! 👋
40 |
41 |
; 42 | ``` 43 | 44 | ## Marker onClick handler 45 | 46 | ```jsx 47 | import React, { useState } from 'react'; 48 | import MapGL, { Marker } from '@urbica/react-map-gl'; 49 | import 'mapbox-gl/dist/mapbox-gl.css'; 50 | 51 | const [position, setPosition] = useState({ 52 | longitude: 0, 53 | latitude: 0 54 | }); 55 | 56 | const style = { 57 | padding: '10px', 58 | color: '#fff', 59 | cursor: 'pointer', 60 | background: '#1978c8', 61 | borderRadius: '6px' 62 | }; 63 | 64 | const onMapClick = (event) => { 65 | setPosition({ longitude: event.lngLat.lng, latitude: event.lngLat.lat }); 66 | }; 67 | 68 | const onMarkerClick = (event) => { 69 | alert('You clicked on marker'); 70 | event.stopPropagation(); 71 | }; 72 | 73 | 82 | 87 |
Click me! ✨
88 |
89 |
; 90 | ``` 91 | -------------------------------------------------------------------------------- /src/components/Marker/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent, ReactNode } from "react"; 2 | import type { Marker as MapboxMarker, PointLike, LngLat } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /** Marker content */ 6 | children: ReactNode; 7 | 8 | /** 9 | * A string indicating the part of the Marker 10 | * that should be positioned closest to the coordinate 11 | */ 12 | anchor?: 13 | | "center" 14 | | "top" 15 | | "bottom" 16 | | "left" 17 | | "right" 18 | | "top-left" 19 | | "top-right" 20 | | "bottom-left" 21 | | "bottom-right"; 22 | 23 | /** The longitude of the center of the marker. */ 24 | longitude: number; 25 | 26 | /** The latitude of the center of the marker. */ 27 | latitude: number; 28 | 29 | /** 30 | * The offset in pixels as a `PointLike` object to apply 31 | * relative to the element's center. Negatives indicate left and up. 32 | */ 33 | offset?: PointLike; 34 | 35 | /** 36 | * Boolean indicating whether or not a marker is able to be dragged 37 | * to a new position on the map. 38 | */ 39 | draggable?: boolean; 40 | 41 | /** 42 | * The rotation angle of the marker in degrees, relative to its 43 | * respective `rotationAlignment` setting. A positive value will 44 | * rotate the marker clockwise. 45 | */ 46 | rotation?: number; 47 | 48 | /** 49 | * map aligns the `Marker` to the plane of the map. `viewport` 50 | * aligns the Marker to the plane of the viewport. `auto` automatically 51 | * matches the value of `rotationAlignment`. 52 | */ 53 | pitchAlignment?: string; 54 | 55 | /** 56 | * map aligns the `Marker`'s rotation relative to the map, maintaining 57 | * a bearing as the map rotates. `viewport` aligns the `Marker`'s rotation 58 | * relative to the viewport, agnostic to map rotations. 59 | * `auto` is equivalent to `viewport`. 60 | */ 61 | rotationAlignment?: string; 62 | 63 | /** Fired when the marker is clicked */ 64 | onClick?: (event: MouseEvent) => void; 65 | 66 | /** Fired when the marker is finished being dragged */ 67 | onDragEnd?: (lngLat: LngLat) => void; 68 | 69 | /** Fired when the marker is finished being dragged */ 70 | onDragStart?: (lngLat: LngLat) => void; 71 | 72 | /** Fired when the marker is dragged */ 73 | onDrag?: (lngLat: LngLat) => void; 74 | }; 75 | 76 | export default class Marker extends PureComponent { 77 | constructor(props: Props); 78 | 79 | getMarker(): MapboxMarker; 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Marker/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createPortal } from 'react-dom'; 4 | import { PureComponent, createElement } from 'react'; 5 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 6 | import type MapboxMarker from 'mapbox-gl/src/ui/marker'; 7 | import type LngLat from 'mapbox-gl/src/geo/lng_lat'; 8 | import type { PointLike } from '@mapbox/point-geometry'; 9 | 10 | import MapContext from '../MapContext'; 11 | import mapboxgl from '../../utils/mapbox-gl'; 12 | 13 | type Props = { 14 | /** Marker content */ 15 | children: React$Node, 16 | 17 | /** 18 | * A string indicating the part of the Marker 19 | * that should be positioned closest to the coordinate 20 | */ 21 | anchor: 22 | | 'center' 23 | | 'top' 24 | | 'bottom' 25 | | 'left' 26 | | 'right' 27 | | 'top-left' 28 | | 'top-right' 29 | | 'bottom-left' 30 | | 'bottom-right', 31 | 32 | /** The longitude of the center of the marker. */ 33 | longitude: number, 34 | 35 | /** The latitude of the center of the marker. */ 36 | latitude: number, 37 | 38 | /** 39 | * The offset in pixels as a `PointLike` object to apply 40 | * relative to the element's center. Negatives indicate left and up. 41 | */ 42 | offset?: PointLike, 43 | 44 | /** 45 | * Boolean indicating whether or not a marker is able to be dragged 46 | * to a new position on the map. 47 | */ 48 | draggable?: boolean, 49 | 50 | /** 51 | * The rotation angle of the marker in degrees, relative to its 52 | * respective `rotationAlignment` setting. A positive value will 53 | * rotate the marker clockwise. 54 | */ 55 | rotation: number, 56 | 57 | /** 58 | * map aligns the `Marker` to the plane of the map. `viewport` 59 | * aligns the Marker to the plane of the viewport. `auto` automatically 60 | * matches the value of `rotationAlignment`. 61 | */ 62 | pitchAlignment: string, 63 | 64 | /** 65 | * map aligns the `Marker`'s rotation relative to the map, maintaining 66 | * a bearing as the map rotates. `viewport` aligns the `Marker`'s rotation 67 | * relative to the viewport, agnostic to map rotations. 68 | * `auto` is equivalent to `viewport`. 69 | */ 70 | rotationAlignment: string, 71 | 72 | /** Fired when the marker is clicked */ 73 | onClick?: () => any, 74 | 75 | /** Fired when the marker is finished being dragged */ 76 | onDragEnd?: (lngLat: LngLat) => any, 77 | 78 | /** Fired when the marker is finished being dragged */ 79 | onDragStart?: (lngLat: LngLat) => any, 80 | 81 | /** Fired when the marker is dragged */ 82 | onDrag?: (lngLat: LngLat) => any 83 | }; 84 | 85 | class Marker extends PureComponent { 86 | _map: MapboxMap; 87 | 88 | _el: HTMLDivElement; 89 | 90 | _marker: MapboxMarker; 91 | 92 | static displayName = 'Marker'; 93 | 94 | static defaultProps = { 95 | anchor: 'center', 96 | offset: null, 97 | draggable: false, 98 | rotation: 0, 99 | pitchAlignment: 'auto', 100 | rotationAlignment: 'auto' 101 | }; 102 | 103 | constructor(props: Props) { 104 | super(props); 105 | this._el = document.createElement('div'); 106 | } 107 | 108 | componentDidMount() { 109 | const { 110 | longitude, 111 | latitude, 112 | onClick, 113 | onDragEnd, 114 | onDragStart, 115 | onDrag 116 | } = this.props; 117 | 118 | this._marker = new mapboxgl.Marker({ 119 | element: this._el, 120 | anchor: this.props.anchor, 121 | draggable: this.props.draggable, 122 | offset: this.props.offset, 123 | rotation: this.props.rotation, 124 | pitchAlignment: this.props.pitchAlignment, 125 | rotationAlignment: this.props.rotationAlignment 126 | }); 127 | 128 | this._marker.setLngLat([longitude, latitude]).addTo(this._map); 129 | 130 | if (onClick) { 131 | this._el.addEventListener('click', onClick); 132 | } 133 | 134 | if (onDragEnd) { 135 | this._marker.on('dragend', this._onDragEnd); 136 | } 137 | 138 | if (onDragStart) { 139 | this._marker.on('dragstart', this._onDragStart); 140 | } 141 | 142 | if (onDrag) { 143 | this._marker.on('drag', this._onDrag); 144 | } 145 | } 146 | 147 | componentDidUpdate(prevProps: Props) { 148 | const positionChanged = 149 | prevProps.latitude !== this.props.latitude || 150 | prevProps.longitude !== this.props.longitude; 151 | 152 | if (positionChanged) { 153 | this._marker.setLngLat([this.props.longitude, this.props.latitude]); 154 | } 155 | } 156 | 157 | componentWillUnmount() { 158 | if (!this._map || !this._map.getStyle()) { 159 | return; 160 | } 161 | 162 | if (this.props.onClick) { 163 | this._el.removeEventListener('click', this.props.onClick); 164 | } 165 | 166 | this._marker.remove(); 167 | } 168 | 169 | getMarker() { 170 | return this._marker; 171 | } 172 | 173 | _onDragEnd = (): void => { 174 | // $FlowFixMe 175 | this.props.onDragEnd(this._marker.getLngLat()); 176 | }; 177 | 178 | _onDragStart = (): void => { 179 | // $FlowFixMe 180 | this.props.onDragStart(this._marker.getLngLat()); 181 | }; 182 | 183 | _onDrag = (): void => { 184 | // $FlowFixMe 185 | this.props.onDrag(this._marker.getLngLat()); 186 | }; 187 | 188 | render() { 189 | return createElement(MapContext.Consumer, {}, (map) => { 190 | if (map) { 191 | this._map = map; 192 | } 193 | 194 | return createPortal(this.props.children, this._el); 195 | }); 196 | } 197 | } 198 | 199 | export default Marker; 200 | -------------------------------------------------------------------------------- /src/components/Marker/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Marker } from '../..'; 6 | 7 | const Element =
ok
; 8 | test('render', () => { 9 | const onDragEnd = jest.fn(); 10 | const onDragStart = jest.fn(); 11 | const onDrag = jest.fn(); 12 | 13 | const wrapper = mount( 14 | 15 | 23 | 24 | ); 25 | 26 | const MarkerWrapper = wrapper.find('Marker'); 27 | expect(MarkerWrapper.exists()).toBe(true); 28 | const marker = MarkerWrapper.instance().getMarker(); 29 | expect(marker).toBeTruthy(); 30 | 31 | wrapper.unmount(); 32 | expect(wrapper.find('Marker').exists()).toBe(false); 33 | }); 34 | 35 | test('update', () => { 36 | const wrapper = mount( 37 | 38 | 39 | 40 | ); 41 | 42 | wrapper.setProps({ 43 | children: 44 | }); 45 | 46 | wrapper.setProps({ 47 | children: 48 | }); 49 | }); 50 | 51 | test('throws', () => { 52 | console.error = jest.fn(); 53 | 54 | expect(() => 55 | mount() 56 | ).toThrow(); 57 | 58 | expect(console.error).toHaveBeenCalled(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/NavigationControl/README.md: -------------------------------------------------------------------------------- 1 | A `NavigationControl` control contains zoom buttons and a compass. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { NavigationControl } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 16 | 17 | ; 18 | ``` 19 | -------------------------------------------------------------------------------- /src/components/NavigationControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type { NavigationControl as MapboxNavigationControl } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /** If true the compass button is included. */ 6 | showCompass?: boolean; 7 | 8 | /** If true the zoom-in and zoom-out buttons are included. */ 9 | showZoom?: boolean; 10 | 11 | /** 12 | * If true the pitch is visualized by rotating X-axis of compass 13 | * and pitch will reset by clicking on the compass. 14 | */ 15 | visualizePitch?: boolean; 16 | 17 | /** A string representing the position of the control on the map. */ 18 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 19 | }; 20 | 21 | export default class NavigationControl extends PureComponent { 22 | getControl(): MapboxNavigationControl; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/NavigationControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxNavigationControl from 'mapbox-gl/src/ui/control/navigation_control'; 6 | 7 | import MapContext from '../MapContext'; 8 | import mapboxgl from '../../utils/mapbox-gl'; 9 | 10 | type Props = { 11 | /** If true the compass button is included. */ 12 | showCompass: boolean, 13 | 14 | /** If true the zoom-in and zoom-out buttons are included. */ 15 | showZoom: boolean, 16 | 17 | /** 18 | * If true the pitch is visualized by rotating X-axis of compass 19 | * and pitch will reset by clicking on the compass. 20 | */ 21 | visualizePitch: boolean, 22 | 23 | /** A string representing the position of the control on the map. */ 24 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 25 | }; 26 | 27 | /** 28 | * A `NavigationControl` control contains zoom buttons and a compass. 29 | */ 30 | class NavigationControl extends PureComponent { 31 | _map: MapboxMap; 32 | 33 | _control: MapboxNavigationControl; 34 | 35 | static defaultProps = { 36 | showCompass: true, 37 | showZoom: true, 38 | visualizePitch: false, 39 | position: 'top-right' 40 | }; 41 | 42 | componentDidMount() { 43 | const map: MapboxMap = this._map; 44 | const { showCompass, showZoom, visualizePitch, position } = this.props; 45 | 46 | const control: MapboxNavigationControl = new mapboxgl.NavigationControl({ 47 | showCompass, 48 | showZoom, 49 | visualizePitch 50 | }); 51 | 52 | map.addControl(control, position); 53 | this._control = control; 54 | } 55 | 56 | componentWillUnmount() { 57 | if (!this._map || !this._map.getStyle()) { 58 | return; 59 | } 60 | 61 | this._map.removeControl(this._control); 62 | } 63 | 64 | getControl() { 65 | return this._control; 66 | } 67 | 68 | render() { 69 | return createElement(MapContext.Consumer, {}, (map) => { 70 | if (map) { 71 | this._map = map; 72 | } 73 | return null; 74 | }); 75 | } 76 | } 77 | 78 | export default NavigationControl; 79 | -------------------------------------------------------------------------------- /src/components/NavigationControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { NavigationControl } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | ); 13 | 14 | const control = wrapper.find('NavigationControl'); 15 | expect(control.exists()).toBe(true); 16 | expect(control.instance().getControl()).toBeTruthy(); 17 | 18 | wrapper.unmount(); 19 | expect(wrapper.find('NavigationControl').exists()).toBe(false); 20 | }); 21 | 22 | test('throws', () => { 23 | console.error = jest.fn(); 24 | expect(() => mount()).toThrow(); 25 | expect(console.error).toHaveBeenCalled(); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/Popup/README.md: -------------------------------------------------------------------------------- 1 | React Component for [Mapbox GL JS Popup](https://docs.mapbox.com/mapbox-gl-js/api/#popup). 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { Popup } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 16 | 17 | Hi there! 👋 18 | 19 | ; 20 | ``` 21 | -------------------------------------------------------------------------------- /src/components/Popup/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent, ReactNode } from "react"; 2 | import type { Popup as MapboxPopup, LngLatBoundsLike } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /** Popup content. */ 6 | children: ReactNode; 7 | 8 | /** The longitude of the center of the popup. */ 9 | longitude: number; 10 | 11 | /** The latitude of the center of the popup. */ 12 | latitude: number; 13 | 14 | /* 15 | * If true, a close button will appear 16 | * in the top right corner of the popup. 17 | */ 18 | closeButton?: boolean; 19 | 20 | /** If true, the popup will closed when the map is clicked. */ 21 | closeOnClick?: boolean; 22 | 23 | /** The onClose callback is fired when the popup closed. */ 24 | onClose?: Function; 25 | 26 | /* 27 | * A string indicating the part of the Popup 28 | * that should be positioned closest to the coordinate. 29 | * */ 30 | anchor?: 31 | | "top" 32 | | "bottom" 33 | | "left" 34 | | "right" 35 | | "top-left" 36 | | "top-right" 37 | | "bottom-left" 38 | | "bottom-right"; 39 | 40 | /** 41 | * The offset in pixels as a `PointLike` object to apply 42 | * relative to the element's center. Negatives indicate left and up. 43 | */ 44 | offset?: LngLatBoundsLike; 45 | 46 | /** The className of the popup */ 47 | className?: string; 48 | 49 | /** A string that sets the CSS property of the popup's maximum width. */ 50 | maxWidth?: string; 51 | }; 52 | 53 | export default class Popup extends PureComponent { 54 | constructor(props: Props); 55 | 56 | getPopup(): MapboxPopup; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Popup/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createPortal } from 'react-dom'; 4 | import { PureComponent, createElement } from 'react'; 5 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 6 | import type MapboxPopup from 'mapbox-gl/src/ui/popup'; 7 | import type MapboxLngLatBoundsLike from 'mapbox-gl/src/geo/lng_lat_bounds'; 8 | 9 | import MapContext from '../MapContext'; 10 | import mapboxgl from '../../utils/mapbox-gl'; 11 | 12 | type Props = { 13 | /** Popup content. */ 14 | children: React$Node, 15 | 16 | /** The longitude of the center of the popup. */ 17 | longitude: number, 18 | 19 | /** The latitude of the center of the popup. */ 20 | latitude: number, 21 | 22 | /* 23 | * If true, a close button will appear 24 | * in the top right corner of the popup. 25 | */ 26 | closeButton?: boolean, 27 | 28 | /** If true, the popup will closed when the map is clicked. */ 29 | closeOnClick?: boolean, 30 | 31 | /** The onClose callback is fired when the popup closed. */ 32 | onClose?: Function, 33 | 34 | /* 35 | * A string indicating the part of the Popup 36 | * that should be positioned closest to the coordinate. 37 | * */ 38 | anchor?: 39 | | 'top' 40 | | 'bottom' 41 | | 'left' 42 | | 'right' 43 | | 'top-left' 44 | | 'top-right' 45 | | 'bottom-left' 46 | | 'bottom-right', 47 | 48 | /** 49 | * The offset in pixels as a `PointLike` object to apply 50 | * relative to the element's center. Negatives indicate left and up. 51 | */ 52 | offset?: MapboxLngLatBoundsLike, 53 | 54 | /** The className of the popup */ 55 | className?: string, 56 | 57 | /** A string that sets the CSS property of the popup's maximum width. */ 58 | maxWidth?: string 59 | }; 60 | 61 | class Popup extends PureComponent { 62 | _map: MapboxMap; 63 | 64 | _el: HTMLDivElement; 65 | 66 | _popup: MapboxPopup; 67 | 68 | static displayName = 'Popup'; 69 | 70 | static defaultProps = { 71 | closeButton: true, 72 | closeOnClick: true, 73 | onClose: undefined, 74 | anchor: undefined, 75 | offset: undefined, 76 | className: undefined, 77 | maxWidth: '240px' 78 | }; 79 | 80 | constructor(props: Props) { 81 | super(props); 82 | this._el = document.createElement('div'); 83 | } 84 | 85 | componentDidMount() { 86 | const { 87 | longitude, 88 | latitude, 89 | offset, 90 | closeButton, 91 | closeOnClick, 92 | onClose, 93 | anchor, 94 | className, 95 | maxWidth 96 | } = this.props; 97 | 98 | this._popup = new mapboxgl.Popup({ 99 | offset, 100 | closeButton, 101 | closeOnClick, 102 | anchor, 103 | className, 104 | maxWidth 105 | }); 106 | 107 | this._popup.setDOMContent(this._el); 108 | this._popup.setLngLat([longitude, latitude]).addTo(this._map); 109 | 110 | if (onClose) { 111 | this._popup.on('close', onClose); 112 | } 113 | } 114 | 115 | componentDidUpdate(prevProps: Props) { 116 | const positionChanged = 117 | prevProps.latitude !== this.props.latitude || 118 | prevProps.longitude !== this.props.longitude; 119 | 120 | if (positionChanged) { 121 | this._popup.setLngLat([this.props.longitude, this.props.latitude]); 122 | } 123 | } 124 | 125 | componentWillUnmount() { 126 | if (!this._map || !this._map.getStyle()) { 127 | return; 128 | } 129 | 130 | this._popup.remove(); 131 | } 132 | 133 | getPopup() { 134 | return this._popup; 135 | } 136 | 137 | render() { 138 | return createElement(MapContext.Consumer, {}, (map) => { 139 | if (map) { 140 | this._map = map; 141 | } 142 | 143 | return createPortal(this.props.children, this._el); 144 | }); 145 | } 146 | } 147 | 148 | export default Popup; 149 | -------------------------------------------------------------------------------- /src/components/Popup/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Popup } from '../..'; 6 | 7 | test('render', () => { 8 | const onClose = jest.fn(); 9 | 10 | const wrapper = mount( 11 | 12 | 13 | Content 14 | 15 | 16 | ); 17 | 18 | const PopupWrapper = wrapper.find('Popup'); 19 | expect(PopupWrapper.exists()).toBe(true); 20 | const popup = PopupWrapper.instance().getPopup(); 21 | expect(popup).toBeTruthy(); 22 | 23 | wrapper.unmount(); 24 | expect(wrapper.find('Popup').exists()).toBe(false); 25 | }); 26 | 27 | test('update', () => { 28 | const wrapper = mount( 29 | 30 | 31 | Content 32 | 33 | 34 | ); 35 | 36 | wrapper.setProps({ 37 | children: ( 38 | 39 | Content 40 | 41 | ) 42 | }); 43 | 44 | wrapper.setProps({ 45 | children: ( 46 | 47 | Content 48 | 49 | ) 50 | }); 51 | }); 52 | 53 | test('throws', () => { 54 | console.error = jest.fn(); 55 | 56 | expect(() => 57 | mount( 58 | 59 | Content 60 | 61 | ) 62 | ).toThrow(); 63 | 64 | expect(console.error).toHaveBeenCalled(); 65 | }); 66 | -------------------------------------------------------------------------------- /src/components/ScaleControl/README.md: -------------------------------------------------------------------------------- 1 | A `ScaleControl` control displays the ratio of a distance on the map to the corresponding distance on the ground. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import MapGL, { ScaleControl } from '@urbica/react-map-gl'; 6 | import 'mapbox-gl/dist/mapbox-gl.css'; 7 | 8 | 16 | 17 | ; 18 | ``` 19 | -------------------------------------------------------------------------------- /src/components/ScaleControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type { ScaleControl as MapboxScaleControl } from "mapbox-gl"; 3 | 4 | type Props = { 5 | /* The maximum length of the scale control in pixels. */ 6 | maxWidth?: number; 7 | 8 | /* Unit of the distance. */ 9 | unit?: "imperial" | "metric" | "nautical"; 10 | 11 | /* A string representing the position of the control on the map. */ 12 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 13 | }; 14 | 15 | export default class ScaleControl extends PureComponent { 16 | getControl(): MapboxScaleControl; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ScaleControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type MapboxScaleControl from 'mapbox-gl/src/ui/control/scale_control'; 6 | 7 | import MapContext from '../MapContext'; 8 | import mapboxgl from '../../utils/mapbox-gl'; 9 | 10 | type Props = { 11 | /* The maximum length of the scale control in pixels. */ 12 | maxWidth: number, 13 | 14 | /* Unit of the distance. */ 15 | unit: 'imperial' | 'metric' | 'nautical', 16 | 17 | /* A string representing the position of the control on the map. */ 18 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 19 | }; 20 | 21 | /** 22 | * A `ScaleControl` control displays the ratio of a distance on the map 23 | * to the corresponding distance on the ground. 24 | */ 25 | class ScaleControl extends PureComponent { 26 | _map: MapboxMap; 27 | 28 | _control: MapboxScaleControl; 29 | 30 | static defaultProps = { 31 | position: 'bottom-right', 32 | unit: 'metric' 33 | }; 34 | 35 | componentDidMount() { 36 | const map: MapboxMap = this._map; 37 | const { maxWidth, unit, position } = this.props; 38 | 39 | const control: MapboxScaleControl = new mapboxgl.ScaleControl({ 40 | maxWidth, 41 | unit 42 | }); 43 | 44 | map.addControl(control, position); 45 | this._control = control; 46 | } 47 | 48 | componentWillUnmount() { 49 | if (!this._map || !this._map.getStyle()) { 50 | return; 51 | } 52 | 53 | this._map.removeControl(this._control); 54 | } 55 | 56 | getControl() { 57 | return this._control; 58 | } 59 | 60 | render() { 61 | return createElement(MapContext.Consumer, {}, (map) => { 62 | if (map) { 63 | this._map = map; 64 | } 65 | return null; 66 | }); 67 | } 68 | } 69 | 70 | export default ScaleControl; 71 | -------------------------------------------------------------------------------- /src/components/ScaleControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { ScaleControl } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | ); 13 | 14 | const control = wrapper.find('ScaleControl'); 15 | expect(control.exists()).toBe(true); 16 | expect(control.instance().getControl()).toBeTruthy(); 17 | 18 | wrapper.unmount(); 19 | expect(wrapper.find('ScaleControl').exists()).toBe(false); 20 | }); 21 | 22 | test('throws', () => { 23 | console.error = jest.fn(); 24 | expect(() => mount()).toThrow(); 25 | expect(console.error).toHaveBeenCalled(); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/Source/README.md: -------------------------------------------------------------------------------- 1 | [Sources](https://docs.mapbox.com/mapbox-gl-js/api/#sources) specify the geographic features to be rendered on the map. 2 | 3 | ## GeoJSON Source 4 | 5 | A [GeoJSON source](https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources-geojson). Data must be provided via a `data` property, whose value can be a URL or inline GeoJSON. 6 | 7 | ```jsx 8 | import React, { useState } from 'react'; 9 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 10 | import 'mapbox-gl/dist/mapbox-gl.css'; 11 | 12 | const [viewport, setViewport] = useState({ 13 | latitude: 45.137451890638886, 14 | longitude: -68.13734351262877, 15 | zoom: 5 16 | }); 17 | 18 | const data = { 19 | type: 'Feature', 20 | geometry: { 21 | type: 'Polygon', 22 | coordinates: [ 23 | [ 24 | [-67.13734351262877, 45.137451890638886], 25 | [-66.96466, 44.8097], 26 | [-68.03252, 44.3252], 27 | [-69.06, 43.98], 28 | [-70.11617, 43.68405], 29 | [-70.64573401557249, 43.090083319667144], 30 | [-70.75102474636725, 43.08003225358635], 31 | [-70.79761105007827, 43.21973948828747], 32 | [-70.98176001655037, 43.36789581966826], 33 | [-70.94416541205806, 43.46633942318431], 34 | [-71.08482, 45.3052400000002], 35 | [-70.6600225491012, 45.46022288673396], 36 | [-70.30495378282376, 45.914794623389355], 37 | [-70.00014034695016, 46.69317088478567], 38 | [-69.23708614772835, 47.44777598732787], 39 | [-68.90478084987546, 47.184794623394396], 40 | [-68.23430497910454, 47.35462921812177], 41 | [-67.79035274928509, 47.066248887716995], 42 | [-67.79141211614706, 45.702585354182816], 43 | [-67.13734351262877, 45.137451890638886] 44 | ] 45 | ] 46 | } 47 | }; 48 | 49 | 56 | 57 | 66 | ; 67 | ``` 68 | 69 | Drawing a GeoJSON line on a map. 70 | 71 | ```jsx 72 | import React, { useState } from 'react'; 73 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 74 | import 'mapbox-gl/dist/mapbox-gl.css'; 75 | 76 | const [viewport, setViewport] = useState({ 77 | latitude: 37.830348, 78 | longitude: -122.486052, 79 | zoom: 15 80 | }); 81 | 82 | const data = { 83 | type: 'Feature', 84 | geometry: { 85 | type: 'LineString', 86 | coordinates: [ 87 | [-122.48369693756104, 37.83381888486939], 88 | [-122.48348236083984, 37.83317489144141], 89 | [-122.48339653015138, 37.83270036637107], 90 | [-122.48356819152832, 37.832056363179625], 91 | [-122.48404026031496, 37.83114119107971], 92 | [-122.48404026031496, 37.83049717427869], 93 | [-122.48348236083984, 37.829920943955045], 94 | [-122.48356819152832, 37.82954808664175], 95 | [-122.48507022857666, 37.82944639795659], 96 | [-122.48610019683838, 37.82880236636284], 97 | [-122.48695850372314, 37.82931081282506], 98 | [-122.48700141906738, 37.83080223556934], 99 | [-122.48751640319824, 37.83168351665737], 100 | [-122.48803138732912, 37.832158048267786], 101 | [-122.48888969421387, 37.83297152392784], 102 | [-122.48987674713133, 37.83263257682617], 103 | [-122.49043464660643, 37.832937629287755], 104 | [-122.49125003814696, 37.832429207817725], 105 | [-122.49163627624512, 37.832564787218985], 106 | [-122.49223709106445, 37.83337825839438], 107 | [-122.49378204345702, 37.83368330777276] 108 | ] 109 | } 110 | }; 111 | 112 | 119 | 120 | 133 | ; 134 | ``` 135 | 136 | ### Updating GeoJSON Source Data 137 | 138 | ```jsx 139 | import React, { useState } from 'react'; 140 | import { randomPoint } from '@turf/random'; 141 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 142 | import 'mapbox-gl/dist/mapbox-gl.css'; 143 | 144 | const [points, setPoints] = useState(randomPoint(100)); 145 | 146 | const [viewport, setViewport] = useState({ 147 | latitude: 0, 148 | longitude: 0, 149 | zoom: 0 150 | }); 151 | 152 | const addPoints = () => { 153 | const randomPoints = randomPoint(100); 154 | const newFeatures = points.features.concat(randomPoints.features); 155 | const newPoints = { ...points, features: newFeatures }; 156 | setPoints(newPoints); 157 | }; 158 | 159 | 160 | 161 | 168 | 169 | 178 | 179 | ; 180 | ``` 181 | 182 | ## Vector Source 183 | 184 | Add a [vector source](https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources-vector) to a map. 185 | 186 | ```jsx 187 | import React, { useState } from 'react'; 188 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 189 | import 'mapbox-gl/dist/mapbox-gl.css'; 190 | 191 | const [viewport, setViewport] = useState({ 192 | latitude: 37.753574, 193 | longitude: -122.447303, 194 | zoom: 13 195 | }); 196 | 197 | 204 | 205 | 215 | ; 216 | ``` 217 | 218 | ## Raster Source 219 | 220 | ```jsx 221 | import React, { useState } from 'react'; 222 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 223 | import 'mapbox-gl/dist/mapbox-gl.css'; 224 | 225 | const [viewport, setViewport] = useState({ 226 | latitude: 40.6892, 227 | longitude: -74.5447, 228 | zoom: 8 229 | }); 230 | 231 | 237 | 243 | 244 | ; 245 | ``` 246 | 247 | ## Dynamic Source URLs 248 | 249 | ```jsx 250 | import React, { useState } from 'react'; 251 | import { randomPoint } from '@turf/random'; 252 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 253 | import 'mapbox-gl/dist/mapbox-gl.css'; 254 | 255 | const sourceURLs = { 256 | first: 'mapbox://stepankuzmin.ck0glwxo402ld2omagmzc2gma-7pqww', 257 | second: 'mapbox://stepankuzmin.ck0glym6u02ls2omawvm9vi4y-9xid1' 258 | }; 259 | 260 | const [sourceURL, setSourceURL] = useState(sourceURLs.first); 261 | 262 | const [viewport, setViewport] = useState({ 263 | latitude: 37.78, 264 | longitude: -122.41, 265 | zoom: 9 266 | }); 267 | 268 | const toggleSourceURL = () => { 269 | const nextSourceURL = sourceURL === sourceURLs.first ? sourceURLs.second : sourceURLs.first; 270 | setSourceURL(nextSourceURL); 271 | }; 272 | 273 | 274 | 275 | 282 | 283 | 293 | 294 | ; 295 | ``` 296 | 297 | ## Dynamic Source Tiles 298 | 299 | ```jsx 300 | import React, { useState } from 'react'; 301 | import { randomPoint } from '@turf/random'; 302 | import MapGL, { Source, Layer } from '@urbica/react-map-gl'; 303 | import 'mapbox-gl/dist/mapbox-gl.css'; 304 | 305 | const sourceTilesURLs = { 306 | toner: 'https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', 307 | watercolor: 'https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png' 308 | }; 309 | 310 | const [sourceTiles, setSourceTiles] = useState(sourceTilesURLs.toner); 311 | 312 | const [viewport, setViewport] = useState({ 313 | latitude: 37.78, 314 | longitude: -122.41, 315 | zoom: 9 316 | }); 317 | 318 | const toggleSourceTiles = () => { 319 | const nextSourceTiles = 320 | sourceTiles === sourceTilesURLs.toner ? sourceTilesURLs.watercolor : sourceTilesURLs.toner; 321 | setSourceTiles(nextSourceTiles); 322 | }; 323 | 324 | 325 | 326 | 333 | 334 | 335 | 336 | ; 337 | ``` 338 | -------------------------------------------------------------------------------- /src/components/Source/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SourceSpecification, 3 | RasterSourceSpecification, 4 | VectorSourceSpecification, 5 | } from "mapbox-gl/src/style-spec/types"; 6 | import { PureComponent, ReactNode } from "react"; 7 | 8 | export type TileSourceSpecification = 9 | | VectorSourceSpecification 10 | | RasterSourceSpecification; 11 | 12 | export type Props = SourceSpecification & { 13 | /** Mapbox GL Source id */ 14 | id: string; 15 | 16 | /** Layers */ 17 | children?: ReactNode; 18 | }; 19 | 20 | type State = { 21 | loaded: boolean; 22 | }; 23 | 24 | export default class Source extends PureComponent {} 25 | -------------------------------------------------------------------------------- /src/components/Source/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PureComponent, createElement } from 'react'; 4 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 5 | import type { ChildrenArray, Element } from 'react'; 6 | import type { 7 | SourceSpecification, 8 | RasterSourceSpecification, 9 | VectorSourceSpecification, 10 | GeoJSONSourceSpecification, 11 | ImageSourceSpecification 12 | } from 'mapbox-gl/src/style-spec/types'; 13 | 14 | import MapContext from '../MapContext'; 15 | import Layer from '../Layer'; 16 | 17 | /* eslint-disable import/no-cycle */ 18 | import isArraysEqual from '../../utils/isArraysEqual'; 19 | import validateSource from '../../utils/validateSource'; 20 | 21 | export type TileSourceSpecification = 22 | | VectorSourceSpecification 23 | | RasterSourceSpecification; 24 | 25 | export type Props = { 26 | /** Mapbox GL Source */ 27 | ...SourceSpecification, 28 | 29 | /** Mapbox GL Source id */ 30 | id: string, 31 | 32 | /** Layers */ 33 | children?: ChildrenArray> 34 | }; 35 | 36 | type State = { 37 | loaded: boolean 38 | }; 39 | 40 | class Source extends PureComponent { 41 | _map: MapboxMap; 42 | 43 | static displayName = 'Source'; 44 | 45 | state = { 46 | loaded: false 47 | }; 48 | 49 | componentDidMount() { 50 | const { id, children, ...restSourceProps } = this.props; 51 | const source = validateSource((restSourceProps: any)); 52 | 53 | this._map.addSource(id, source); 54 | this._map.on('sourcedata', this._onSourceData); 55 | } 56 | 57 | componentDidUpdate(prevProps: Props) { 58 | const { 59 | id: prevId, 60 | children: prevChildren, 61 | ...prevSourceProps 62 | } = prevProps; 63 | const prevSource = validateSource((prevSourceProps: any)); 64 | 65 | const { id, children, ...restSourceProps } = this.props; 66 | const source = validateSource((restSourceProps: any)); 67 | 68 | if (id !== prevId || source.type !== prevSource.type) { 69 | this._map.removeSource(prevId); 70 | this._map.addSource(id, source); 71 | return; 72 | } 73 | 74 | if (source.type === 'geojson' && prevSource.type === 'geojson') { 75 | this._updateGeoJSONSource(id, prevSource, source); 76 | return; 77 | } 78 | 79 | if (source.type === 'image' && prevSource.type === 'image') { 80 | this._updateImageSource(id, prevSource, source); 81 | return; 82 | } 83 | 84 | if (source.type === 'vector' && prevSource.type === 'vector') { 85 | this._updateTileSource(id, prevSource, source); 86 | return; 87 | } 88 | 89 | if (source.type === 'raster' && prevSource.type === 'raster') { 90 | this._updateTileSource(id, prevSource, source); 91 | } 92 | } 93 | 94 | componentWillUnmount() { 95 | if (!this._map || !this._map.getStyle()) { 96 | return; 97 | } 98 | 99 | this._removeSource(); 100 | } 101 | 102 | _onSourceData = () => { 103 | if (!this._map.isSourceLoaded(this.props.id)) { 104 | return; 105 | } 106 | 107 | this._map.off('sourcedata', this._onSourceData); 108 | this.setState({ loaded: true }); 109 | }; 110 | 111 | _updateGeoJSONSource = ( 112 | id: string, 113 | prevSource: GeoJSONSourceSpecification, 114 | newSource: GeoJSONSourceSpecification 115 | ) => { 116 | if (newSource.data !== prevSource.data) { 117 | const source = this._map.getSource(id); 118 | 119 | if (source !== undefined) { 120 | source.setData(newSource.data); 121 | } 122 | } 123 | }; 124 | 125 | _updateImageSource = ( 126 | id: string, 127 | prevSource: ImageSourceSpecification, 128 | newSource: ImageSourceSpecification 129 | ) => { 130 | if ( 131 | newSource.url !== prevSource.url || 132 | newSource.coordinates !== prevSource.coordinates 133 | ) { 134 | const source = this._map.getSource(id); 135 | if (source !== undefined) { 136 | source.updateImage(newSource); 137 | } 138 | } 139 | }; 140 | 141 | // https://github.com/mapbox/mapbox-gl-js/pull/8048 142 | _updateTileSource = ( 143 | id: string, 144 | prevSource: TileSourceSpecification, 145 | newSource: TileSourceSpecification 146 | ) => { 147 | if ( 148 | newSource.url === prevSource.url && 149 | isArraysEqual(newSource.tiles, prevSource.tiles) 150 | ) { 151 | return; 152 | } 153 | 154 | const source = this._map.getSource(id); 155 | 156 | /* eslint-disable no-underscore-dangle */ 157 | if (source._tileJSONRequest) { 158 | source._tileJSONRequest.cancel(); 159 | } 160 | 161 | source.url = newSource.url; 162 | source.scheme = newSource.scheme; 163 | source._options = { ...source._options, ...newSource }; 164 | /* eslint-enable no-underscore-dangle */ 165 | 166 | const sourceCache = this._map.style.sourceCaches[id]; 167 | if (sourceCache) { 168 | sourceCache.clearTiles(); 169 | } 170 | 171 | source.load(); 172 | }; 173 | 174 | _removeSource = () => { 175 | const { id } = this.props; 176 | if (this._map.getSource(id)) { 177 | const { layers } = this._map.getStyle(); 178 | if (layers) { 179 | layers.forEach((layer) => { 180 | if (layer.source === id) { 181 | this._map.removeLayer(layer.id); 182 | } 183 | }); 184 | } 185 | 186 | this._map.removeSource(id); 187 | } 188 | }; 189 | 190 | render() { 191 | const { loaded } = this.state; 192 | const { children } = this.props; 193 | 194 | return createElement(MapContext.Consumer, {}, (map: ?MapboxMap) => { 195 | if (map) { 196 | this._map = map; 197 | } 198 | 199 | // $FlowFixMe 200 | return loaded && children; 201 | }); 202 | } 203 | } 204 | 205 | export default Source; 206 | -------------------------------------------------------------------------------- /src/components/Source/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { Source, Layer } from '../..'; 6 | 7 | test('render geojson source', () => { 8 | const data = { type: 'FeatureCollection', features: [] }; 9 | 10 | const wrapper = mount( 11 | 12 | 13 | 14 | ); 15 | 16 | expect(wrapper.find('Source').exists()).toBe(true); 17 | 18 | wrapper.unmount(); 19 | expect(wrapper.find('Source').exists()).toBe(false); 20 | }); 21 | 22 | test('update geojson source', () => { 23 | const data1 = { type: 'FeatureCollection', features: [] }; 24 | const data2 = { type: 'FeatureCollection', features: [] }; 25 | 26 | const wrapper = mount( 27 | 28 | 29 | 30 | ); 31 | 32 | wrapper.setProps({ 33 | children: 34 | }); 35 | }); 36 | 37 | test('render image source', () => { 38 | const data = { url: '', coordinates: [] }; 39 | 40 | const wrapper = mount( 41 | 42 | 48 | 49 | ); 50 | 51 | expect(wrapper.find('Source').exists()).toBe(true); 52 | 53 | wrapper.unmount(); 54 | expect(wrapper.find('Source').exists()).toBe(false); 55 | }); 56 | 57 | test('update image source', () => { 58 | const data1 = { url: '1', coordinates: [] }; 59 | const data2 = { url: '2', coordinates: [] }; 60 | 61 | const wrapper = mount( 62 | 63 | 69 | 70 | ); 71 | 72 | wrapper.setProps({ 73 | children: ( 74 | 80 | ) 81 | }); 82 | }); 83 | 84 | test('render vector source', () => { 85 | const wrapper = mount( 86 | 87 | 88 | 89 | ); 90 | 91 | expect(wrapper.find('Source').exists()).toBe(true); 92 | 93 | wrapper.unmount(); 94 | expect(wrapper.find('Source').exists()).toBe(false); 95 | }); 96 | 97 | test('update vector source url', () => { 98 | const wrapper = mount( 99 | 100 | 101 | 102 | ); 103 | 104 | wrapper.setProps({ 105 | children: ( 106 | 107 | ) 108 | }); 109 | }); 110 | 111 | test('update raster source url', () => { 112 | const wrapper = mount( 113 | 114 | 115 | 116 | ); 117 | 118 | wrapper.setProps({ 119 | children: 120 | }); 121 | }); 122 | 123 | test('update vector source tiles', () => { 124 | const wrapper = mount( 125 | 126 | 131 | 132 | ); 133 | 134 | wrapper.setProps({ 135 | children: ( 136 | 141 | ) 142 | }); 143 | }); 144 | 145 | test('update raster source tiles', () => { 146 | const wrapper = mount( 147 | 148 | 153 | 154 | ); 155 | 156 | wrapper.setProps({ 157 | children: ( 158 | 163 | ) 164 | }); 165 | }); 166 | 167 | test('remove and add new source', () => { 168 | const data = { type: 'FeatureCollection', features: [] }; 169 | 170 | const wrapper = mount( 171 | 172 | 173 | 174 | ); 175 | 176 | wrapper.setProps({ 177 | children: ( 178 | 179 | ) 180 | }); 181 | }); 182 | 183 | test('throws', () => { 184 | console.error = jest.fn(); 185 | const data = { type: 'FeatureCollection', features: [] }; 186 | 187 | expect(() => 188 | mount() 189 | ).toThrow(); 190 | 191 | expect(console.error).toHaveBeenCalled(); 192 | }); 193 | 194 | test('do not render children until loaded', () => { 195 | /* eslint-disable global-require */ 196 | const mapboxgl = require('../../__mocks__/mapbox-gl'); 197 | mapboxgl.Map.prototype.isSourceLoaded = () => false; 198 | jest.setMock('mapbox-gl', mapboxgl); 199 | 200 | const data = { type: 'FeatureCollection', features: [] }; 201 | const wrapper = mount( 202 | 203 | 204 | 205 | 206 | 207 | ); 208 | 209 | expect(wrapper.find('Layer').exists()).toBe(false); 210 | 211 | wrapper.find('Source').setState({ loaded: true }); 212 | expect(wrapper.find('Layer').exists()).toBe(true); 213 | }); 214 | -------------------------------------------------------------------------------- /src/components/TrafficControl/README.md: -------------------------------------------------------------------------------- 1 | A `TrafficControl` add control to toggle traffic on map. See [Mapbox-gl-traffic 2 | ](https://github.com/mapbox/mapbox-gl-traffic) examples. 3 | 4 | ⚠️ Requires the `@mapbox/mapbox-gl-traffic` package to be installed: 5 | 6 | ```shell 7 | npm install --save @mapbox/mapbox-gl-traffic 8 | ``` 9 | 10 | ...or 11 | 12 | ```shell 13 | yarn add @mapbox/mapbox-gl-traffic 14 | ``` 15 | 16 | ```js 17 | import React, { useState } from 'react'; 18 | import MapGL, { TrafficControl } from '@urbica/react-map-gl'; 19 | import 'mapbox-gl/dist/mapbox-gl.css'; 20 | import '@mapbox/mapbox-gl-traffic/mapbox-gl-traffic.css'; 21 | 22 | const [showTraffic, setShowTraffic] = useState(false); 23 | const [showTrafficButton, setShowTrafficButton] = useState(false); 24 | 25 | const toggleTraffic = () => setShowTraffic(!showTraffic); 26 | const toggleButton = () => setShowTrafficButton(!showTrafficButton); 27 | 28 |
29 | 30 | 31 | 39 | 40 | 41 |
; 42 | ``` 43 | -------------------------------------------------------------------------------- /src/components/TrafficControl/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PureComponent } from "react"; 2 | import type MapboxTrafficControl from "@mapbox/mapbox-gl-traffic"; 3 | 4 | type Props = { 5 | /** Show or hide traffic overlay by default. */ 6 | showTraffic?: Boolean; 7 | 8 | /** Show a toggle button to turn traffic on and off. */ 9 | showTrafficButton?: Boolean; 10 | 11 | /** 12 | * The traffic source regex used to determine whether a layer displays 13 | * traffic or not 14 | * */ 15 | trafficSource?: RegExp; 16 | }; 17 | 18 | export default class TrafficControl extends PureComponent { 19 | getControl(): MapboxTrafficControl; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/TrafficControl/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { PureComponent, createElement } from 'react'; 3 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 4 | import type MapboxTrafficControl from '@mapbox/mapbox-gl-traffic'; 5 | 6 | import MapboxTraffic from '@mapbox/mapbox-gl-traffic'; 7 | import MapContext from '../MapContext'; 8 | 9 | type Props = { 10 | /** Show or hide traffic overlay by default. */ 11 | showTraffic?: Boolean, 12 | 13 | /** Show a toggle button to turn traffic on and off. */ 14 | showTrafficButton?: Boolean, 15 | 16 | /** 17 | * The traffic source regex used to determine whether a layer displays 18 | * traffic or not 19 | * */ 20 | trafficSource?: RegExp 21 | }; 22 | 23 | /** Add a traffic toggle control. */ 24 | class TrafficControl extends PureComponent { 25 | _map: MapboxMap; 26 | 27 | _control: MapboxTrafficControl; 28 | 29 | static defaultProps = { 30 | showTraffic: false, 31 | showTrafficButton: true, 32 | trafficSource: /mapbox-traffic-v\d/ 33 | }; 34 | 35 | componentDidMount() { 36 | this._addControl(); 37 | } 38 | 39 | componentDidUpdate(prevProps: Props) { 40 | if (prevProps.showTraffic !== this.props.showTraffic) { 41 | this._control.toggleTraffic(); 42 | } 43 | 44 | const shouldUpdate = 45 | prevProps.showTrafficButton !== this.props.showTrafficButton || 46 | prevProps.trafficSource !== this.props.trafficSource; 47 | 48 | if (shouldUpdate) { 49 | this._map.removeControl(this._control); 50 | 51 | this._addControl(); 52 | } 53 | } 54 | 55 | componentWillUnmount() { 56 | if (!this._map || !this._map.getStyle()) { 57 | return; 58 | } 59 | 60 | this._map.removeControl(this._control); 61 | } 62 | 63 | _addControl = () => { 64 | const { showTraffic, showTrafficButton, trafficSource } = this.props; 65 | 66 | const control = new MapboxTraffic({ 67 | showTraffic, 68 | showTrafficButton, 69 | trafficSource 70 | }); 71 | 72 | this._map.addControl(control); 73 | this._control = control; 74 | }; 75 | 76 | getControl() { 77 | return this._control; 78 | } 79 | 80 | render() { 81 | return createElement(MapContext.Consumer, {}, (map) => { 82 | if (map) { 83 | this._map = map; 84 | } 85 | return null; 86 | }); 87 | } 88 | } 89 | 90 | export default TrafficControl; 91 | -------------------------------------------------------------------------------- /src/components/TrafficControl/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import MapGL, { TrafficControl, Layer } from '../..'; 6 | 7 | test('render', () => { 8 | const wrapper = mount( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | const control = wrapper.find('TrafficControl'); 16 | expect(control.exists()).toBe(true); 17 | expect(control.instance().getControl()).toBeTruthy(); 18 | wrapper.setProps({ 19 | children: [] 20 | }); 21 | wrapper.setProps({ 22 | children: [] 23 | }); 24 | 25 | wrapper.unmount(); 26 | expect(wrapper.find('TrafficControl').exists()).toBe(false); 27 | }); 28 | 29 | test('throws', () => { 30 | console.error = jest.fn(); 31 | 32 | expect(() => mount()).toThrow(); 33 | expect(console.error).toHaveBeenCalled(); 34 | }); 35 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default, Viewport } from "./components/MapGL"; 2 | 3 | export { default as MapContext } from "./components/MapContext"; 4 | export { default as Layer } from "./components/Layer"; 5 | export { default as CustomLayer } from "./components/CustomLayer"; 6 | export { default as Source } from "./components/Source"; 7 | export { default as Popup } from "./components/Popup"; 8 | export { default as Marker } from "./components/Marker"; 9 | export { default as FeatureState } from "./components/FeatureState"; 10 | export { default as Image } from "./components/Image"; 11 | export { default as AttributionControl } from "./components/AttributionControl"; 12 | export { default as FullscreenControl } from "./components/FullscreenControl"; 13 | export { default as GeolocateControl } from "./components/GeolocateControl"; 14 | export { default as NavigationControl } from "./components/NavigationControl"; 15 | export { default as ScaleControl } from "./components/ScaleControl"; 16 | export { default as LanguageControl } from "./components/LanguageControl"; 17 | export { default as TrafficControl } from "./components/TrafficControl"; 18 | export { default as Filter } from "./components/Filter"; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable import/no-cycle, no-restricted-exports */ 4 | export { default } from './components/MapGL'; 5 | /* eslint-enable import/no-cycle, no-restricted-exports */ 6 | 7 | export { default as MapContext } from './components/MapContext'; 8 | export { default as Layer } from './components/Layer'; 9 | export { default as CustomLayer } from './components/CustomLayer'; 10 | export { default as Source } from './components/Source'; 11 | export { default as Popup } from './components/Popup'; 12 | export { default as Marker } from './components/Marker'; 13 | export { default as FeatureState } from './components/FeatureState'; 14 | export { default as Image } from './components/Image'; 15 | export { default as AttributionControl } from './components/AttributionControl'; 16 | export { default as FullscreenControl } from './components/FullscreenControl'; 17 | export { default as GeolocateControl } from './components/GeolocateControl'; 18 | export { default as NavigationControl } from './components/NavigationControl'; 19 | export { default as ScaleControl } from './components/ScaleControl'; 20 | export { default as LanguageControl } from './components/LanguageControl'; 21 | export { default as TrafficControl } from './components/TrafficControl'; 22 | export { default as Filter } from './components/Filter'; 23 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | 6 | jest.mock('mapbox-gl'); 7 | 8 | global.process.browser = true; 9 | -------------------------------------------------------------------------------- /src/utils/capitalizeFirstLetter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | function capitalizeFirstLetter(string: string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1); 5 | } 6 | 7 | module.exports = capitalizeFirstLetter; 8 | -------------------------------------------------------------------------------- /src/utils/diff.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type KV = { 4 | [string]: any 5 | }; 6 | 7 | const diff = (newProps: KV = {}, prevProps: KV = {}) => { 8 | const keys = new Set([...Object.keys(newProps), ...Object.keys(prevProps)]); 9 | return [...keys].reduce((acc, key: string) => { 10 | const value = newProps[key]; 11 | if (value !== prevProps[key]) { 12 | acc.push([key, value]); 13 | } 14 | return acc; 15 | }, []); 16 | }; 17 | 18 | export default diff; 19 | -------------------------------------------------------------------------------- /src/utils/diff.test.js: -------------------------------------------------------------------------------- 1 | import diff from './diff'; 2 | 3 | test('diff#undefined', () => { 4 | expect(diff(undefined, undefined)).toEqual([]); 5 | }); 6 | 7 | test('diff#empty', () => { 8 | const prevMap = {}; 9 | const newMap = {}; 10 | 11 | expect(diff(newMap, prevMap)).toEqual([]); 12 | }); 13 | 14 | test('diff#add', () => { 15 | const prevMap = { a: 1, b: 2 }; 16 | const newMap = { a: 1, b: 2, c: 3, d: 4 }; 17 | 18 | expect(diff(newMap, prevMap)).toEqual([['c', 3], ['d', 4]]); 19 | }); 20 | 21 | test('diff#remove', () => { 22 | const prevMap = { a: 1, b: 2 }; 23 | const newMap = {}; 24 | 25 | expect(diff(newMap, prevMap)).toEqual([['a', undefined], ['b', undefined]]); 26 | }); 27 | 28 | test('diff#override', () => { 29 | const prevMap = { a: 1, b: 2 }; 30 | const newMap = { a: 3, b: 4 }; 31 | 32 | expect(diff(newMap, prevMap)).toEqual([['a', 3], ['b', 4]]); 33 | }); 34 | 35 | test('diff#nested', () => { 36 | const prevMap = { a: 1, b: { c: 2 } }; 37 | const newMap = { a: 1, b: { c: 3 } }; 38 | 39 | expect(diff(newMap, prevMap)).toEqual([['b', { c: 3 }]]); 40 | }); 41 | -------------------------------------------------------------------------------- /src/utils/generateEventProps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const events = require('../components/MapGL/events'); 6 | const capitalizeFirstLetter = require('./capitalizeFirstLetter'); 7 | 8 | const propsList = events.map( 9 | event => ` /** The ${event} event handler */ 10 | on${capitalizeFirstLetter(event)}?: Function` 11 | ); 12 | 13 | const template = props => `// @flow 14 | 15 | export type EventProps = { 16 | ${props.join(',\n\n')} 17 | }; 18 | `; 19 | 20 | const eventProps = path.join(__dirname, '../components/MapGL/eventProps.js'); 21 | fs.writeFileSync(eventProps, template(propsList)); 22 | -------------------------------------------------------------------------------- /src/utils/isArraysEqual.js: -------------------------------------------------------------------------------- 1 | const isArraysEqual = (arr1 = [], arr2 = []) => { 2 | if (arr1.length !== arr2.length) { 3 | return false; 4 | } 5 | 6 | for (let index = 0; index < arr1.length; index += 1) { 7 | if (arr1[index] !== arr2[index]) { 8 | return false; 9 | } 10 | } 11 | 12 | return true; 13 | }; 14 | 15 | export default isArraysEqual; 16 | -------------------------------------------------------------------------------- /src/utils/isArraysEqual.test.js: -------------------------------------------------------------------------------- 1 | import isArraysEqual from './isArraysEqual'; 2 | 3 | test('isArraysEqual#empty', () => { 4 | expect(isArraysEqual([], [])).toEqual(true); 5 | }); 6 | 7 | test('isArraysEqual#length', () => { 8 | expect(isArraysEqual(['test1'], ['test1', 'test2'])).toEqual(false); 9 | }); 10 | 11 | test('isArraysEqual#equal', () => { 12 | expect(isArraysEqual(['test'], ['test'])).toEqual(true); 13 | }); 14 | 15 | test('isArraysEqual#equal2', () => { 16 | expect(isArraysEqual(['test1', 'test2'], ['test1', 'test2'])).toEqual(true); 17 | }); 18 | 19 | test('isArraysEqual#notEqual', () => { 20 | expect(isArraysEqual(['test1'], ['test2'])).toEqual(false); 21 | }); 22 | 23 | test('isArraysEqual#notEqual2', () => { 24 | expect(isArraysEqual(['test1', 'test1'], ['test1', 'test2'])).toEqual(false); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils/isBrowser.js: -------------------------------------------------------------------------------- 1 | // based on https://github.com/uber/luma.gl/blob/master/src/utils/is-browser.js 2 | // This function is needed in initialization stages, 3 | // make sure it can be imported in isolation 4 | import isElectron from './isElectron'; 5 | 6 | const isNode = 7 | typeof process === 'object' && 8 | String(process) === '[object process]' && 9 | !process.browser; 10 | 11 | const isBrowser = !isNode || isElectron; 12 | 13 | // document does not exist on worker thread 14 | export const isBrowserMainThread = isBrowser && typeof document !== 'undefined'; 15 | 16 | export default isBrowser; 17 | -------------------------------------------------------------------------------- /src/utils/isElectron.js: -------------------------------------------------------------------------------- 1 | // based on https://github.com/uber/luma.gl/blob/master/src/utils/is-electron.js 2 | function isElectron() { 3 | // Renderer process 4 | if (typeof window !== 'undefined' && typeof window.process === 'object' && 5 | window.process.type === 'renderer') { 6 | return true; 7 | } 8 | // Main process 9 | if (typeof process !== 'undefined' && typeof process.versions === 'object' && 10 | Boolean(process.versions.electron)) { 11 | return true; 12 | } 13 | // Detect the user agent when the `nodeIntegration` option is set to true 14 | if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && 15 | navigator.userAgent.indexOf('Electron') >= 0) { 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | export default isElectron(); 22 | -------------------------------------------------------------------------------- /src/utils/mapbox-gl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type MapboxGL from 'mapbox-gl/src/index'; 3 | import isBrowser from './isBrowser'; 4 | 5 | // $FlowFixMe 6 | const mapboxgl: MapboxGL = isBrowser ? require('mapbox-gl') : null; 7 | 8 | export default mapboxgl; 9 | -------------------------------------------------------------------------------- /src/utils/normalizeChildren.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Children, cloneElement } from 'react'; 4 | import type { Element } from 'react'; 5 | 6 | import Layer from '../components/Layer'; 7 | import CustomLayer from '../components/CustomLayer'; 8 | import type { Children as MapChildren } from '../components/MapGL'; 9 | 10 | type LayerLike = Element | Element; 11 | 12 | const LayerLikeTypes = [Layer, CustomLayer]; 13 | const isLayerLike = (child: Element) => 14 | LayerLikeTypes.includes(child.type); 15 | 16 | const getLayerId = (child: LayerLike): string => 17 | // $FlowFixMe 18 | child.props.id || child.props.layer.id; 19 | 20 | const forEachLayer = (fn, children: MapChildren) => { 21 | Children.forEach(children, (child) => { 22 | if (!child) return; 23 | if (isLayerLike(child)) fn(child); 24 | if (child.props && child.props.children) 25 | forEachLayer(fn, child.props.children); 26 | }); 27 | }; 28 | 29 | const getLayerIds = (children: MapChildren): Array => { 30 | const layerIds = []; 31 | forEachLayer((child) => { 32 | if (!child.props.before) { 33 | layerIds.push(getLayerId(child)); 34 | } 35 | }, children); 36 | return layerIds; 37 | }; 38 | 39 | const normalizeChildren = (children: MapChildren) => { 40 | const layerIds = getLayerIds(children); 41 | layerIds.shift(); 42 | 43 | const traverse = (_children: MapChildren) => { 44 | if (typeof _children === 'function') { 45 | return _children; 46 | } 47 | 48 | return Children.map(_children, (child: Element) => { 49 | if (!child) { 50 | return child; 51 | } 52 | 53 | if (isLayerLike(child)) { 54 | const before: string = child.props.before || layerIds.shift(); 55 | return cloneElement(child, { before }); 56 | } 57 | 58 | if (child.props && child.props.children) { 59 | return cloneElement(child, { 60 | children: traverse(child.props.children) 61 | }); 62 | } 63 | 64 | return child; 65 | }); 66 | }; 67 | 68 | const normalizedChildren = traverse(children); 69 | return normalizedChildren; 70 | }; 71 | 72 | export default normalizeChildren; 73 | -------------------------------------------------------------------------------- /src/utils/point.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const point = ( 4 | coordinates: [number, number], 5 | properties: { [string]: any } = {} 6 | ) => ({ 7 | type: 'Feature', 8 | properties, 9 | geometry: { 10 | type: 'Point', 11 | coordinates 12 | } 13 | }); 14 | 15 | export default point; 16 | -------------------------------------------------------------------------------- /src/utils/queryRenderedFeatures.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type MapboxMap from 'mapbox-gl/src/ui/map'; 3 | 4 | const queryRenderedFeatures = ( 5 | map: MapboxMap, 6 | layerId: string, 7 | position: [number, number], 8 | radius: number 9 | ) => { 10 | const parameters = { layers: [layerId] }; 11 | 12 | if (radius) { 13 | const bbox = [ 14 | [position[0] - radius, position[1] - radius], 15 | [position[0] + radius, position[1] + radius] 16 | ]; 17 | 18 | return map.queryRenderedFeatures(bbox, parameters); 19 | } 20 | 21 | return map.queryRenderedFeatures(position, parameters); 22 | }; 23 | 24 | export default queryRenderedFeatures; 25 | -------------------------------------------------------------------------------- /src/utils/queryRenderedFeatures.test.js: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | import queryRenderedFeatures from './queryRenderedFeatures'; 3 | 4 | const createMap = () => { 5 | const container = window.document.createElement('div'); 6 | Object.defineProperty(container, 'offsetWidth', { 7 | value: 200, 8 | configurable: true 9 | }); 10 | Object.defineProperty(container, 'offsetHeight', { 11 | value: 200, 12 | configurable: true 13 | }); 14 | 15 | const map = new mapboxgl.Map({ 16 | container, 17 | interactive: false, 18 | attributionControl: false, 19 | trackResize: true, 20 | style: { 21 | version: 8, 22 | sources: {}, 23 | layers: [] 24 | } 25 | }); 26 | 27 | return map; 28 | }; 29 | 30 | test('queryRenderedFeatures', () => { 31 | const map = createMap(); 32 | const layerId = 'dummy'; 33 | const position = [0, 0]; 34 | const radius = 10; 35 | 36 | expect(queryRenderedFeatures(map, layerId, position)).toEqual([]); 37 | expect(queryRenderedFeatures(map, layerId, position, radius)).toEqual([]); 38 | }); 39 | -------------------------------------------------------------------------------- /src/utils/shallowCompareChildren.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Children } from 'react'; 4 | import type { Node } from 'react'; 5 | 6 | const childrenKeys = (children: Node): string[] => 7 | Children.toArray(children).map(child => child.key); 8 | 9 | const shallowCompareChildren = ( 10 | prevChildren: Node, 11 | newChildren: Node 12 | ): boolean => { 13 | if (Children.count(prevChildren) !== Children.count(newChildren)) { 14 | return false; 15 | } 16 | 17 | const prevKeys = childrenKeys(prevChildren); 18 | const newKeys = new Set(childrenKeys(newChildren)); 19 | return prevKeys.every(key => newKeys.has(key)); 20 | }; 21 | 22 | export default shallowCompareChildren; 23 | -------------------------------------------------------------------------------- /src/utils/shallowCompareChildren.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import shallowCompareChildren from './shallowCompareChildren'; 4 | 5 | test('shallowCompareChildren#undefined', () => { 6 | const prevChildren = shallow(
    ) 7 | .find('ul') 8 | .children(); 9 | 10 | const newChildren = shallow(
      ) 11 | .find('ul') 12 | .children(); 13 | 14 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(true); 15 | }); 16 | 17 | test('diff#one-1', () => { 18 | const prevChildren = shallow( 19 |
        20 |
      • 21 |
      22 | ) 23 | .find('ul') 24 | .children(); 25 | 26 | const newChildren = shallow( 27 |
        28 |
      • 29 |
      30 | ) 31 | .find('ul') 32 | .children(); 33 | 34 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(true); 35 | }); 36 | 37 | test('diff#one-2', () => { 38 | const prevChildren = shallow( 39 |
        40 |
      • 41 |
      42 | ) 43 | .find('ul') 44 | .children(); 45 | 46 | const newChildren = shallow( 47 |
        48 |
      • 49 |
      50 | ) 51 | .find('ul') 52 | .children(); 53 | 54 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(false); 55 | }); 56 | 57 | test('diff#multiple-1', () => { 58 | const prevChildren = shallow( 59 |
        60 |
      • 61 |
      • 62 |
      • 63 |
      64 | ) 65 | .find('ul') 66 | .children(); 67 | 68 | const newChildren = shallow( 69 |
        70 |
      • 71 |
      • 72 |
      • 73 |
      74 | ) 75 | .find('ul') 76 | .children(); 77 | 78 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(true); 79 | }); 80 | 81 | test('diff#multiple-2', () => { 82 | const prevChildren = shallow( 83 |
        84 |
      • 85 |
      • 86 |
      • 87 |
      88 | ) 89 | .find('ul') 90 | .children(); 91 | 92 | const newChildren = shallow( 93 |
        94 |
      • 95 |
      • 96 |
      • 97 |
      98 | ) 99 | .find('ul') 100 | .children(); 101 | 102 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(false); 103 | }); 104 | 105 | test('diff#different-count', () => { 106 | const prevChildren = shallow( 107 |
        108 |
      • 109 |
      110 | ) 111 | .find('ul') 112 | .children(); 113 | 114 | const newChildren = shallow( 115 |
        116 |
      • 117 |
      • 118 |
      119 | ) 120 | .find('ul') 121 | .children(); 122 | 123 | expect(shallowCompareChildren(prevChildren, newChildren)).toEqual(false); 124 | }); 125 | -------------------------------------------------------------------------------- /src/utils/validateSource.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | SourceSpecification, 4 | RasterSourceSpecification, 5 | RasterDEMSourceSpecification, 6 | VectorSourceSpecification, 7 | GeoJSONSourceSpecification, 8 | ImageSourceSpecification, 9 | VideoSourceSpecification 10 | } from 'mapbox-gl/src/style-spec/types'; 11 | 12 | export default (props: SourceSpecification): SourceSpecification => { 13 | switch (props.type) { 14 | case 'vector': { 15 | const source: VectorSourceSpecification = { type: 'vector', ...props }; 16 | return source; 17 | } 18 | case 'raster': { 19 | const source: RasterSourceSpecification = { type: 'raster', ...props }; 20 | return source; 21 | } 22 | case 'raster-dem': { 23 | const source: RasterDEMSourceSpecification = { 24 | type: 'raster-dem', 25 | ...props 26 | }; 27 | return source; 28 | } 29 | case 'geojson': { 30 | const source: GeoJSONSourceSpecification = { type: 'geojson', ...props }; 31 | return source; 32 | } 33 | case 'video': { 34 | const source: VideoSourceSpecification = { type: 'video', ...props }; 35 | return source; 36 | } 37 | case 'image': { 38 | const source: ImageSourceSpecification = { type: 'image', ...props }; 39 | return source; 40 | } 41 | default: 42 | throw new Error(`Unknown type for '${props.id}' Source`); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/validateSource.test.js: -------------------------------------------------------------------------------- 1 | import validateSource from './validateSource'; 2 | 3 | const types = ['vector', 'raster', 'raster-dem', 'geojson', 'video', 'image']; 4 | 5 | test('validate source', () => { 6 | types.forEach((type) => { 7 | const source = { type }; 8 | expect(validateSource(source).type).toEqual(type); 9 | }); 10 | 11 | expect(() => validateSource({ type: 'invalid' })).toThrow(); 12 | }); 13 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | title: 'Urbica React Map GL', 6 | usageMode: 'expand', 7 | exampleMode: 'expand', 8 | pagePerSection: true, 9 | assetsDir: 'src/assets/', 10 | require: [path.resolve(__dirname, 'styleguide.setup.js')], 11 | moduleAliases: { 12 | '@urbica/react-map-gl': path.resolve(__dirname, 'src') 13 | }, 14 | sections: [ 15 | { 16 | name: 'Introduction', 17 | content: 'docs/introduction.md' 18 | }, 19 | { 20 | name: 'Installation', 21 | content: 'docs/installation.md' 22 | }, 23 | { 24 | name: 'Components', 25 | sectionDepth: 1, 26 | components: [ 27 | 'src/components/MapGL/index.js', 28 | 'src/components/Source/index.js', 29 | 'src/components/Layer/index.js', 30 | 'src/components/CustomLayer/index.js', 31 | 'src/components/Image/index.js', 32 | 'src/components/Popup/index.js', 33 | 'src/components/Marker/index.js', 34 | 'src/components/FeatureState/index.js', 35 | 'src/components/Filter/index.js' 36 | ] 37 | }, 38 | { 39 | name: 'Controls', 40 | sectionDepth: 1, 41 | components: [ 42 | 'src/components/AttributionControl/index.js', 43 | 'src/components/FullscreenControl/index.js', 44 | 'src/components/GeolocateControl/index.js', 45 | 'src/components/NavigationControl/index.js', 46 | 'src/components/ScaleControl/index.js', 47 | 'src/components/LanguageControl/index.js', 48 | 'src/components/TrafficControl/index.js' 49 | ] 50 | } 51 | ], 52 | webpackConfig: { 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.jsx?$/, 57 | exclude: /node_modules/, 58 | loader: 'babel-loader' 59 | }, 60 | { 61 | test: /\.css$/, 62 | use: ['style-loader', 'css-loader'] 63 | } 64 | ] 65 | } 66 | }, 67 | dangerouslyUpdateWebpackConfig: (webpackConfig) => { 68 | const envPlugin = new webpack.EnvironmentPlugin(['MAPBOX_ACCESS_TOKEN']); 69 | webpackConfig.plugins.push(envPlugin); 70 | return webpackConfig; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /styleguide.setup.js: -------------------------------------------------------------------------------- 1 | import 'mapbox-gl/dist/mapbox-gl.css'; 2 | 3 | global.MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN; 4 | --------------------------------------------------------------------------------