├── .babelrc ├── .editorconfig ├── .github └── workflows │ ├── nodejs.yml │ └── npm-release.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── MIGRATION.md ├── README.md ├── SECURITY.md ├── __mocks__ ├── azure-maps-control.js └── styleMock.js ├── _config.yml ├── assets └── coverage.png ├── babel.config.js ├── commitlint.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── preview ├── react-preview.html └── react-preview.jsx ├── rollup.config.js ├── src ├── components │ ├── AzureMap │ │ ├── AzureMap.test.tsx │ │ ├── AzureMap.tsx │ │ ├── __snapshots__ │ │ │ └── AzureMap.test.tsx.snap │ │ ├── useCreateMapControl.test.tsx │ │ ├── useCreateMapControls.tsx │ │ ├── useCreateSprites.test.tsx │ │ └── useCreateSprites.tsx │ ├── AzureMapFeature │ │ ├── AzureMapFeature.test.tsx │ │ ├── AzureMapFeature.tsx │ │ ├── useCreateAzureMapFeature.test.tsx │ │ ├── useCreateAzureMapFeature.ts │ │ ├── useFeature.test.tsx │ │ └── useFeature.ts │ ├── AzureMapMarkers │ │ └── AzureMapHtmlMarker │ │ │ ├── AzureMapHtmlMarker.test.tsx │ │ │ ├── AzureMapHtmlMarker.tsx │ │ │ └── __snapshots__ │ │ │ └── AzureMapHtmlMarker.test.tsx.snap │ ├── AzureMapPopup │ │ ├── AzureMapPopup.test.tsx │ │ ├── AzureMapPopup.tsx │ │ ├── useCreateAzureMapPopup.test.tsx │ │ └── useCreateAzureMapPopup.ts │ └── helpers │ │ ├── mapHelper.test.ts │ │ └── mapHelper.ts ├── contexts │ ├── AzureMapContext.test.tsx │ ├── AzureMapContext.tsx │ ├── AzureMapDataSourceContext.test.tsx │ ├── AzureMapDataSourceContext.tsx │ ├── AzureMapLayerContext.test.tsx │ ├── AzureMapLayerContext.tsx │ ├── AzureMapVectorTileSourceProvider.test.tsx │ ├── AzureMapVectorTileSourceProvider.tsx │ └── __snapshots__ │ │ └── AzureMapLayerContext.test.tsx.snap ├── hooks │ ├── constructLayer.test.tsx │ ├── useAzureMapLayer.test.tsx │ ├── useAzureMapLayer.tsx │ ├── useCheckRef.test.tsx │ └── useCheckRef.tsx ├── react-azure-maps.ts └── types.ts ├── tools └── semantic-release-prepare.ts ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": "last 2 Firefox versions, last 2 Chrome versions, last 2 Edge versions, last 2 Safari versions" 8 | } } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [16.x, 18.x, 20.x, 22.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | - run: npm ci 31 | - run: npm run test # runs linting and tests 32 | - run: npm run build --if-present 33 | - run: python -m pip install linkcheckmd 34 | - run: python -m linkcheckmd README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to NPM when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Publish Node.js Package to NPM 5 | 6 | on: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - run: npm ci --legacy-peer-deps 20 | - run: npm run build --if-present 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm ci 32 | - run: npm run build --if-present --isNpmBuild 33 | - run: npm publish --access=public --tag=latest 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | preview-build 14 | .eslintcache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "semi": false, 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '11' 10 | - '8' 11 | - '6' 12 | script: 13 | - npm run test:prod && npm run build 14 | after_success: 15 | - npm run travis-deploy-once "npm run report-coverage" 16 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run deploy-docs"; fi 17 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run semantic-release"; fi 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 202020 WiredSolutions 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | ## Migrating from v0.x to v1.0 2 | 3 | ### azure-maps-control dependency 4 | `azure-maps-control` is installed as a peerDependencies package, you will need to add it to your package.json. 5 | ``` 6 | npm install --save azure-maps-control@latest 7 | ``` 8 | This will install `azure-maps-control` v3 to your application. You may upgrade it independently in the future. See [AzureMaps WebSDK release notes](https://learn.microsoft.com/azure/azure-maps/release-notes-map-control) for a list of new features and bug fixes. 9 | 10 | ### Styling 11 | v1.0 removes the internal css import from `azure-maps-control` to accommodate usage in Next.js. You will need to add the following stylesheet to your application manually. The stylesheet is required for the marker, popup and control components in `react-azure-maps` to work properly. 12 | ```javascript 13 | import 'azure-maps-control/dist/atlas.min.css' 14 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Azure-Maps 2 | 3 | This project is community-driven initiative originally created by amazing [@psrednicki](https://github.com/psrednicki), [@msasinowski](https://github.com/msasinowski) and [@tbajda](https://github.com/tbajda) and is now maintained by the Azure Maps team. 4 | 5 | [![npm](https://img.shields.io/npm/v/react-azure-maps.svg) ![npm](https://img.shields.io/npm/dm/react-azure-maps.svg)](https://www.npmjs.com/package/react-azure-maps) [![license](https://img.shields.io/npm/l/react-azure-maps.svg)](https://github.com/Azure/react-azure-maps/blob/master/LICENSE) 6 | 7 | `React Azure Maps` is a react wrapper for [Azure Maps](https://azure.microsoft.com/pl-pl/services/azure-maps/). The whole library is written in typescript and uses React 16.8+ 8 | 9 | ## Installation 10 | 11 | Use the package manager `npm` or `yarn` 12 | 13 | ```bash 14 | npm install react-azure-maps 15 | ``` 16 | 17 | or 18 | 19 | ```bash 20 | yarn add react-azure-maps 21 | ``` 22 | 23 | ## Styling 24 | Embed the following css to your application. The stylesheet is required for the marker, popup and control components in `react-azure-maps` to work properly. 25 | ```javascript 26 | import 'azure-maps-control/dist/atlas.min.css' 27 | ``` 28 | 29 | ## Documentation 30 | 31 | Documentation is available [Documentation](https://react-azure-maps.now.sh) 32 | 33 | Generated documentation from typedoc is available [Documentation](https://azure.github.io/react-azure-maps/) 34 | 35 | ## Compatibility with azure-maps-controls 36 | 37 | ``` 38 | 1.0.0 - 3.0.0 39 | 0.2.0 - 2.0.32 40 | 0.1.4 - 2.0.31 41 | 0.1.3 - 2.0.25 42 | ``` 43 | 44 | ## Playground 45 | 46 | `React Azure Maps` have a fully documented [Playground Package](https://github.com/Azure/react-azure-maps-playground) that implements a lot of features from [Azure Maps Code Samples](https://samples.azuremaps.com/). If you implement new usage of the map and want to be contributor just create a PR. 47 | 48 | ## Library Implementation Details 49 | 50 | For typescript integration and core functionalities, this library uses the newest version of [Azure Maps Control](https://www.npmjs.com/package/azure-maps-control). 51 | The library is implemented under the hood on `Contexts` and uses all benefits of new react features, like new context API, hooks, etc. Across the whole library, there are three main references that depend on the basic `Azure Maps API` 52 | 53 | `MapReference` which is stored and implemented in 54 | 55 | ```javascript 56 | AzureMapsProvider 57 | ``` 58 | 59 | `DataSourceReference` which is stored and implemented in 60 | 61 | ```javascript 62 | AzureMapDataSourceProvider 63 | ``` 64 | 65 | `LayerReference` which is stored and implemented in 66 | 67 | ```javascript 68 | AzureMapLayerProvider 69 | ``` 70 | 71 | If you want to directly make some changes in the above refs just use one of these contexts and feel free to use it any way you want. 72 | The library implements a lot of ready to use components like `AzureMapFeature, AzureMapHTMLMarker, AzureMapPopup` 73 | 74 | ## Basic Usage 75 | 76 | ```javascript 77 | import React from 'react' 78 | import {AzureMap, AzureMapsProvider, IAzureMapOptions, AuthenticationType} from 'react-azure-maps' 79 | 80 | const option: IAzureMapOptions = { 81 | authOptions: { 82 | authType: AuthenticationType.subscriptionKey, 83 | subscriptionKey: '' // Your subscription key 84 | }, 85 | } 86 | 87 | const DefaultMap: React.FC = () => ( 88 | 89 |
90 | 91 |
92 |
93 | ); 94 | 95 | export default DefaultMap 96 | ``` 97 | 98 | ## Authentication 99 | 100 | The subscription key is intended for development environments only and must not be utilized in a production application. Azure Maps provides various authentication options for applications to use. See [here](https://learn.microsoft.com/en-us/azure/azure-maps/how-to-manage-authentication) for more details. 101 | 102 | ```javascript 103 | // AAD 104 | authOptions: { 105 | authType: AuthenticationType.aad, 106 | clientId: '...', 107 | aadAppId: '...', 108 | aadTenant: '...' 109 | } 110 | ``` 111 | 112 | ```javascript 113 | // Anonymous 114 | authOptions: { 115 | authType: AuthenticationType.anonymous, 116 | clientId: '...', 117 | getToken: (resolve, reject) => { 118 | // URL to your authentication service that retrieves an Azure Active Directory Token. 119 | var tokenServiceUrl = "https://example.com/api/GetAzureMapsToken"; 120 | fetch(tokenServiceUrl).then(r => r.text()).then(token => resolve(token)); 121 | } 122 | } 123 | ``` 124 | 125 | ```javascript 126 | // SAS Token 127 | authOptions: { 128 | authType: AuthenticationType.sas, 129 | getToken: (resolve, reject) => { 130 | // URL to your authentication service that retrieves a SAS Token. 131 | var tokenServiceUrl = "https://example.com/api/GetSASToken"; 132 | fetch(tokenServiceUrl).then(r => r.text()).then(token => resolve(token)); 133 | } 134 | } 135 | ``` 136 | 137 | ## Local development with [Playground Package](https://github.com/Azure/react-azure-maps-playground) 138 | 139 | If you want to do some local development using [Playground Package](https://github.com/Azure/react-azure-maps-playground) with local link to the package, you need to make the following steps: 140 | 141 | ```bash 142 | - run yarn watch in `react-azure-maps` package 143 | - run yarn link in `react-azure-maps` package 144 | - go to the `azure-maps-playground` or any other folder or repository and run `yarn link "react-azure-maps"` 145 | ``` 146 | 147 | ## Code coverage 148 | 149 | ![Alt text](assets/coverage.png?raw=true 'Coverage') 150 | 151 | ## Contributing 152 | 153 | Pull requests are welcomed. For major changes, please open an issue first to discuss what you would like to change. 154 | 155 | ## Creators ✨ 156 | 157 | 158 | 159 | 160 | 161 | 162 | 182 | 202 | 221 | 222 |
163 |
psrednicki

170 |
171 | 180 |
181 |
183 |
msasinowski
190 |
191 | 200 |
201 |
203 |
tbajda
210 |
211 | 219 |
220 |
223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | ## License 231 | 232 | [MIT](https://choosealicense.com/licenses/mit/) 233 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /__mocks__/azure-maps-control.js: -------------------------------------------------------------------------------- 1 | class DataSource { 2 | id 3 | options 4 | 5 | constructor(id, options) { 6 | this.id = id 7 | this.options = options 8 | } 9 | 10 | add = jest.fn() 11 | clear = jest.fn() 12 | remove = jest.fn() 13 | importDataFromUrl = jest.fn() 14 | setOptions = jest.fn((options) => (this.options = options)) 15 | getId = () => this.id 16 | } 17 | 18 | module.exports = { 19 | Map: jest.fn(() => ({ 20 | controls: { 21 | add: jest.fn() 22 | }, 23 | events: { 24 | add: jest.fn((_eventName, _targetOrCallback, callback = () => {}) => { 25 | if (typeof _targetOrCallback === 'function') { 26 | _targetOrCallback() 27 | } else { 28 | callback() 29 | } 30 | }), 31 | remove: jest.fn((eventName) => {}) 32 | }, 33 | imageSprite: { 34 | add: jest.fn(), 35 | createFromTemplate: jest.fn() 36 | }, 37 | sources: { 38 | add: jest.fn(), 39 | remove: jest.fn() 40 | }, 41 | layers: { 42 | add: jest.fn(), 43 | remove: jest.fn(), 44 | getLayers: jest.fn(() => []), 45 | getLayerById: jest.fn() 46 | }, 47 | popups: { 48 | getPopups: jest.fn(() => []), 49 | remove: jest.fn() 50 | }, 51 | markers: { 52 | add: jest.fn(), 53 | remove: jest.fn() 54 | }, 55 | setTraffic: jest.fn(), 56 | setUserInteraction: jest.fn(), 57 | setCamera: jest.fn(), 58 | setStyle: jest.fn(), 59 | setServiceOptions: jest.fn() 60 | })), 61 | 62 | HtmlMarker: jest.fn((...args) => ({ 63 | args, 64 | setOptions: jest.fn(), 65 | getOptions: jest.fn(() => ({ 66 | popup: { 67 | isOpen: jest.fn(() => true), 68 | open: jest.fn(() => false), 69 | togglePopup: jest.fn(), 70 | setOptions: jest.fn(), 71 | close: jest.fn() 72 | } 73 | })) 74 | })), 75 | data: { 76 | LineString: jest.fn(() => ({})), 77 | Position: jest.fn(() => ({})) 78 | }, 79 | Pixel: jest.fn(() => ({ 80 | getHeading: jest.fn(() => 'Heading') 81 | })), 82 | Popup: jest.fn(() => ({ 83 | setOptions: jest.fn(), 84 | isOpen: jest.fn(() => false), 85 | open: jest.fn(), 86 | close: jest.fn() 87 | })), 88 | control: { 89 | CompassControl: jest.fn(() => ({ compassOption: 'option' })), 90 | PitchControl: jest.fn(() => ({ pitchOption: 'option' })), 91 | StyleControl: jest.fn(() => ({ styleOption: 'option' })), 92 | ZoomControl: jest.fn(() => ({ zoomOption: 'option' })), 93 | TrafficControl: jest.fn(() => ({ trafficOption: 'option' })), 94 | TrafficLegendControl: jest.fn(() => ({ trafficLegendOption: 'option' })), 95 | ScaleControl: jest.fn(() => ({ scaleOption: 'option' })), 96 | FullscreenControl: jest.fn(() => ({ fullscreenOption: 'option' })) 97 | }, 98 | layer: { 99 | ImageLayer: jest.fn((options, id) => ({ layer: 'ImageLayer', options, id })), 100 | TileLayer: jest.fn((options, id) => ({ layer: 'TileLayer', options, id })), 101 | SymbolLayer: jest.fn((options, id, datasourceRef) => ({ 102 | layer: 'SymbolLayer', 103 | options, 104 | id, 105 | datasourceRef, 106 | setOptions: jest.fn(), 107 | getId: jest.fn(() => id) 108 | })), 109 | HeatMapLayer: jest.fn((options, id, datasourceRef) => ({ 110 | layer: 'HeatLayer', 111 | options, 112 | id, 113 | datasourceRef 114 | })), 115 | LineLayer: jest.fn((options, id, datasourceRef) => ({ 116 | layer: 'LineLayer', 117 | options, 118 | id, 119 | datasourceRef 120 | })), 121 | PolygonExtrusionLayer: jest.fn((options, id, datasourceRef) => ({ 122 | layer: 'PolygonExtrusionLayer', 123 | options, 124 | id, 125 | datasourceRef 126 | })), 127 | PolygonLayer: jest.fn((options, id, datasourceRef) => ({ 128 | layer: 'PolygonLayer', 129 | options, 130 | id, 131 | datasourceRef 132 | })), 133 | BubbleLayer: jest.fn((options, id, datasourceRef) => ({ 134 | layer: 'BubbleLayer', 135 | options, 136 | id, 137 | datasourceRef 138 | })) 139 | }, 140 | source: { 141 | DataSource, 142 | VectorTileSource: jest.fn((id, options) => ({ 143 | getId: jest.fn(() => id), 144 | getOptions: jest.fn(() => options) 145 | })) 146 | }, 147 | Shape: jest.fn(() => ({ 148 | setCoordinates: jest.fn(), 149 | setProperties: jest.fn() 150 | })), 151 | data: { 152 | Position: jest.fn((...args) => args), 153 | BoundingBox: jest.fn((...args) => args), 154 | Point: jest.fn((coords) => ({ coords, type: 'Point' })), 155 | MultiPoint: jest.fn((coords, bbox) => ({ coords, bbox, type: 'MultiPoint' })), 156 | LineString: jest.fn((coords, bbox) => ({ coords, bbox, type: 'LineString' })), 157 | MultiLineString: jest.fn((multipleCoordinates, bbox) => ({ 158 | multipleCoordinates, 159 | bbox, 160 | type: 'MultiLineString' 161 | })), 162 | Polygon: jest.fn((coords, bbox) => ({ coords, bbox, type: 'Polygon' })), 163 | MultiPolygon: jest.fn((multipleDimensionCoordinates, bbox) => ({ 164 | multipleDimensionCoordinates, 165 | bbox, 166 | type: 'MultiPolygon' 167 | })), 168 | Feature: jest.fn() 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /assets/coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/react-azure-maps/59825cbc6be09144e8685d6c1cf2a2cfebde5144/assets/coverage.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | // Use config in .browserlistrc to decide the target env 5 | require('@babel/preset-env'), 6 | { 7 | // Preserve ES modules. Leave module handling to Rollup. 8 | modules: false, 9 | }, 10 | ], 11 | require('@babel/preset-react'), 12 | require('@babel/preset-typescript'), 13 | ], 14 | plugins: [ 15 | // Target project using this preset will need @babel/runtime as a run-time dependency 16 | require('@babel/plugin-transform-runtime'), 17 | 18 | // Stage 3 Proposals 19 | // Public and private instance fields : https://github.com/tc39/proposal-class-fields 20 | // Static class features : https://github.com/tc39/proposal-static-class-features 21 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 22 | 23 | // Finished Proposal - Published in ES 2020 24 | // Optional Chaining : https://github.com/tc39/proposal-optional-chaining 25 | require('@babel/plugin-proposal-optional-chaining'), 26 | 27 | // Finished Proposal - Published in ES 2020 28 | // https://github.com/tc39-transfer/proposal-nullish-coalescing 29 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest' 4 | }, 5 | globals: { 6 | window: {} 7 | }, 8 | testEnvironment: 'node', 9 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.(tsx?|ts?)$', 10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 11 | moduleNameMapper: { 12 | '\\.(css|scss)$': '/__mocks__/styleMock.js' 13 | }, 14 | coveragePathIgnorePatterns: ['/node_modules/', '/test/', `/src/my-example-lib.ts`], 15 | // coverageThreshold: { 16 | // global: { 17 | // branches: 90, 18 | // functions: 95, 19 | // lines: 95, 20 | // statements: 95 21 | // } 22 | // }, 23 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-azure-maps", 3 | "version": "1.0.3", 4 | "description": "React Wrapper for Azure Maps", 5 | "keywords": [ 6 | "react", 7 | "reactjs", 8 | "typescript", 9 | "azure", 10 | "azure-maps", 11 | "azure-maps-control", 12 | "map", 13 | "maps", 14 | "react-azure-maps" 15 | ], 16 | "module": "dist/react-azure-maps.es5.js", 17 | "types": "dist/types/react-azure-maps.d.ts", 18 | "exports": { 19 | ".": { 20 | "import": "./dist/react-azure-maps.es5.js", 21 | "types": "./dist/types/react-azure-maps.d.ts" 22 | } 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "author": "WiredSolutions and Microsoft", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/Azure/react-azure-maps" 31 | }, 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">=8.0.0" 35 | }, 36 | "scripts": { 37 | "lint": "eslint --max-warnings 0 \"./src/**/*.{js,jsx,mjs,ts,tsx}\"", 38 | "format": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx,css,scss,sass,mdx}\"", 39 | "type-check": "tsc --noEmit", 40 | "commitlint": "commitlint", 41 | "commitmsg": "commitlint -e $GIT_PARAMS", 42 | "build:clean": "rimraf dist", 43 | "build:code": "cross-env NODE_ENV=production rollup --config", 44 | "build:types": "tsc --emitDeclarationOnly", 45 | "build": "npm run build:types && npm run build:code", 46 | "test": "npx jest --coverage --env=jsdom", 47 | "test:watch": "jest --coverage --watch --env=jsdom", 48 | "test:prod": "npm run lint && npm run test -- --no-cache", 49 | "dev:code": "cross-env NODE_ENV=development rollup --config --watch", 50 | "preview:react": "npx parcel preview/react-preview.html --out-dir preview-build --open --no-cache", 51 | "dev": "concurrently 'npm:dev:code' 'npm:preview:react'" 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 56 | "pre-commit": "lint-staged" 57 | } 58 | }, 59 | "lint-staged": { 60 | "*.{js,jsx,mjs,ts,tsx}": [ 61 | "eslint --cache --fix" 62 | ], 63 | "*.{js,jsx,json,ts,tsx,css,scss,sass,mdx}": [ 64 | "prettier --write" 65 | ] 66 | }, 67 | "eslintConfig": { 68 | "extends": [ 69 | "react-app", 70 | "react-app/jest" 71 | ] 72 | }, 73 | "devDependencies": { 74 | "@babel/core": "^7.13.10", 75 | "@babel/plugin-proposal-class-properties": "^7.10.4", 76 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 77 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 78 | "@babel/plugin-transform-runtime": "^7.11.5", 79 | "@babel/preset-env": "^7.11.5", 80 | "@babel/preset-react": "^7.10.4", 81 | "@babel/preset-typescript": "^7.10.4", 82 | "@commitlint/cli": "^11.0.0", 83 | "@commitlint/config-angular": "^8.2.0", 84 | "@commitlint/config-conventional": "^11.0.0", 85 | "@rollup/plugin-babel": "^5.2.1", 86 | "@rollup/plugin-commonjs": "^16.0.0", 87 | "@rollup/plugin-html": "^0.2.0", 88 | "@rollup/plugin-json": "^4.1.0", 89 | "@rollup/plugin-node-resolve": "^10.0.0", 90 | "@rollup/plugin-replace": "^2.3.4", 91 | "@testing-library/jest-dom": "^5.11.4", 92 | "@testing-library/react": "^16.2.0", 93 | "@testing-library/user-event": "^12.1.10", 94 | "@types/jest": "^26.0.15", 95 | "@types/react": "19.0.0", 96 | "@types/react-dom": "19.0.0", 97 | "azure-maps-control": "^3.5.0", 98 | "babel-preset-env": "^1.7.0", 99 | "concurrently": "^5.3.0", 100 | "cross-env": "^7.0.2", 101 | "deasync": "^0.1.30", 102 | "eslint": "^7.14.0", 103 | "husky": "^4.3.0", 104 | "lint-staged": "^10.5.2", 105 | "parcel-bundler": "1.12.3", 106 | "prettier": "^2.2.0", 107 | "react": "^19.0.0", 108 | "react-dom": "^19.0.0", 109 | "react-scripts": "^5.0.1", 110 | "rollup": "^2.33.3", 111 | "rollup-plugin-copy": "^3.4.0", 112 | "rollup-plugin-livereload": "^2.0.0", 113 | "rollup-plugin-node-externals": "^2.2.0", 114 | "rollup-plugin-peer-deps-external": "^2.2.4", 115 | "rollup-plugin-postcss": "^3.1.8", 116 | "rollup-plugin-serve": "^1.1.0", 117 | "rollup-plugin-size-snapshot": "^0.12.0", 118 | "rollup-plugin-terser": "^7.0.2", 119 | "ts-jest": "^26.4.4", 120 | "typescript": "^4.1.2" 121 | }, 122 | "peerDependencies": { 123 | "azure-maps-control": "^3.5.0", 124 | "react": "^17.0.2 || ^18.0.0 || ^19.0.0", 125 | "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" 126 | }, 127 | "dependencies": { 128 | "guid-typescript": "^1.0.9" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /preview/react-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preview/react-preview.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { AzureMap, AzureMapsProvider, AuthenticationType } from '../dist/react-azure-maps.es5' 4 | import 'azure-maps-control/dist/atlas.min.css' 5 | 6 | const option = { 7 | authOptions: { 8 | authType: AuthenticationType.subscriptionKey, 9 | subscriptionKey: '' 10 | } 11 | } 12 | 13 | const DefaultMap = () => ( 14 | 15 |
16 | 17 |
18 |
19 | ) 20 | 21 | export default DefaultMap 22 | 23 | createRoot(document.getElementById('root')).render() 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import json from '@rollup/plugin-json' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import replace from '@rollup/plugin-replace' 6 | import externals from 'rollup-plugin-node-externals' 7 | import pkg from './package.json' 8 | import postcss from 'rollup-plugin-postcss' 9 | import { terser } from 'rollup-plugin-terser' 10 | 11 | const ENV_PRODUCTION = 'production' 12 | const ENV_DEVELOPMENT = 'development' 13 | const env = process.env.NODE_ENV || ENV_PRODUCTION 14 | 15 | if (env !== ENV_DEVELOPMENT && env !== ENV_PRODUCTION) { 16 | console.error(` 17 | Unsupported NODE_ENV: ${env} 18 | Should be either "${ENV_DEVELOPMENT}" or "${ENV_PRODUCTION}" 19 | `) 20 | process.exit(1) 21 | } 22 | const extensions = ['.js', '.jsx', '.ts', '.tsx'] 23 | 24 | export default { 25 | input: `src/${pkg.name}.ts`, 26 | output: [ 27 | { 28 | file: pkg.module, 29 | format: 'es', 30 | exports: 'named', 31 | preserveModulesRoot: 'src' 32 | } 33 | ].filter(Boolean), 34 | watch: { 35 | include: 'src/**' 36 | }, 37 | plugins: [ 38 | externals({ peerDeps: true, deps: true }), 39 | replace({ 40 | 'process.env.NODE_ENV': JSON.stringify(env), 41 | preventAssignment: true 42 | }), 43 | json(), 44 | postcss({ 45 | extensions: ['.css'] 46 | }), 47 | resolve({ 48 | browser: true, 49 | extensions 50 | }), 51 | commonjs(), 52 | babel({ 53 | rootMode: 'upward', 54 | extensions, 55 | babelHelpers: 'runtime', 56 | include: ['./src/**/*'] 57 | }), 58 | terser() 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/components/AzureMap/AzureMap.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, act } from '@testing-library/react' 3 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 4 | import AzureMap from './AzureMap' 5 | import { Map } from 'azure-maps-control' 6 | import { IAzureMap, IAzureMapsContextProps } from '../../types' 7 | import { createImageSprites } from './useCreateSprites' 8 | import { createMapCustomControls, createMapControls } from './useCreateMapControls' 9 | 10 | const LoaderComponent = () =>
Loader
11 | 12 | jest.mock('./useCreateMapControls', () => { 13 | return { 14 | createMapCustomControls: jest.fn(), 15 | createMapControls: jest.fn() 16 | } 17 | }) 18 | 19 | jest.mock('./useCreateSprites') 20 | jest.mock('guid-typescript', () => { 21 | return { 22 | Guid: { 23 | create: jest.fn(() => 'fake_generated_id') 24 | } 25 | } 26 | }) 27 | 28 | const mapContextProps = { 29 | mapRef: null, 30 | isMapReady: false, 31 | setMapReady: jest.fn(), 32 | removeMapRef: jest.fn(), 33 | setMapRef: jest.fn() 34 | } 35 | 36 | const wrapWithAzureMapContext = (mapContextProps: IAzureMapsContextProps, mapProps: IAzureMap) => { 37 | return ( 38 | 43 | 44 | 45 | ) 46 | } 47 | 48 | describe('AzureMap Component', () => { 49 | beforeEach(() => { 50 | mapContextProps.removeMapRef.mockClear() 51 | mapContextProps.setMapReady.mockClear() 52 | mapContextProps.setMapRef.mockClear() 53 | }) 54 | 55 | it('should setMapRef on mount', () => { 56 | act(() => { 57 | render(wrapWithAzureMapContext(mapContextProps, {})) 58 | }) 59 | expect(mapContextProps.setMapRef).toHaveBeenCalled() 60 | }) 61 | 62 | it('should change trafficOptions call setTraffic from mapRef', () => { 63 | const mapRef = new Map('fake', {}) 64 | act(() => { 65 | const { rerender } = render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, {})) 66 | rerender( 67 | wrapWithAzureMapContext( 68 | { ...mapContextProps, mapRef }, 69 | { trafficOptions: { some: 'some2' } } 70 | ) 71 | ) 72 | }) 73 | expect(mapRef.setTraffic).toHaveBeenCalledWith({ some: 'some2' }) 74 | }) 75 | 76 | it('should change userInteraction call setUserInteraction from mapRef', () => { 77 | const mapRef = new Map('fake', {}) 78 | act(() => { 79 | const { rerender } = render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, {})) 80 | rerender( 81 | wrapWithAzureMapContext( 82 | { ...mapContextProps, mapRef }, 83 | { userInteraction: { some: 'some2' } } 84 | ) 85 | ) 86 | }) 87 | expect(mapRef.setUserInteraction).toHaveBeenCalledWith({ some: 'some2' }) 88 | }) 89 | 90 | it('should change cameraOptions call setCamera from mapRef', () => { 91 | const mapRef = new Map('fake', {}) 92 | act(() => { 93 | const { rerender } = render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, {})) 94 | rerender( 95 | wrapWithAzureMapContext( 96 | { ...mapContextProps, mapRef }, 97 | { cameraOptions: { some: 'some2' } } 98 | ) 99 | ) 100 | }) 101 | expect(mapRef.setCamera).toHaveBeenCalledWith({ some: 'some2' }) 102 | }) 103 | 104 | it('should call removeMapRef on unmount of component', () => { 105 | const mapRef = new Map('fake', {}) 106 | const { unmount } = render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, {})) 107 | unmount() 108 | expect(mapContextProps.removeMapRef).toHaveBeenCalled() 109 | }) 110 | 111 | it('should call createImageSprites if imageSprites is not falsy', () => { 112 | const mapRef = new Map('fake', {}) 113 | render( 114 | wrapWithAzureMapContext( 115 | { ...mapContextProps, mapRef }, 116 | { imageSprites: [{ id: 'some_fake_id' }] } 117 | ) 118 | ) 119 | expect(createImageSprites).toHaveBeenCalled() 120 | }) 121 | 122 | it('should call createMapControls if controls is not falsy', () => { 123 | const mapRef = new Map('fake', {}) 124 | const fakeControls = [{ controlName: 'fake_control_name' }] 125 | render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, { controls: fakeControls })) 126 | expect(createMapControls).toHaveBeenCalledWith(expect.any(Object), fakeControls) 127 | }) 128 | 129 | it('should call createMapCustomControls if customControls is not falsy', () => { 130 | const mapRef = new Map('fake', {}) 131 | const customControls = [ 132 | { 133 | control: { onAdd: jest.fn(), onRemove: jest.fn() }, 134 | controlOptions: {} 135 | } 136 | ] 137 | render( 138 | wrapWithAzureMapContext( 139 | { ...mapContextProps, mapRef }, 140 | { 141 | customControls 142 | } 143 | ) 144 | ) 145 | expect(createMapCustomControls).toHaveBeenCalledWith(expect.any(Object), customControls) 146 | }) 147 | 148 | it('should setTraffic on initial props', () => { 149 | const mapRef = new Map('fake', {}) 150 | render( 151 | wrapWithAzureMapContext({ ...mapContextProps, mapRef }, { trafficOptions: { some: 'some2' } }) 152 | ) 153 | expect(mapRef.setTraffic).toHaveBeenCalledWith({ some: 'some2' }) 154 | }) 155 | 156 | it('should userInteraction on initial props', () => { 157 | const mapRef = new Map('fake', {}) 158 | render( 159 | wrapWithAzureMapContext( 160 | { ...mapContextProps, mapRef }, 161 | { userInteraction: { some: 'some2' } } 162 | ) 163 | ) 164 | expect(mapRef.setUserInteraction).toHaveBeenCalledWith({ some: 'some2' }) 165 | }) 166 | 167 | it('should cameraOptions on initial props', () => { 168 | const mapRef = new Map('fake', {}) 169 | render( 170 | wrapWithAzureMapContext({ ...mapContextProps, mapRef }, { cameraOptions: { some: 'some2' } }) 171 | ) 172 | expect(mapRef.setCamera).toHaveBeenCalledWith({ some: 'some2' }) 173 | }) 174 | 175 | it('should setStyle on initial props', () => { 176 | const mapRef = new Map('fake', {}) 177 | render( 178 | wrapWithAzureMapContext({ ...mapContextProps, mapRef }, { styleOptions: { some: 'some2' } }) 179 | ) 180 | expect(mapRef.setStyle).toHaveBeenCalledWith({ some: 'some2' }) 181 | }) 182 | 183 | it('should setServiceOptions on initial props', () => { 184 | const mapRef = new Map('fake', {}) 185 | render( 186 | wrapWithAzureMapContext({ ...mapContextProps, mapRef }, { serviceOptions: { some: 'some2' } }) 187 | ) 188 | expect(mapRef.setServiceOptions).toHaveBeenCalledWith({ some: 'some2' }) 189 | }) 190 | 191 | it('should call setMapready on mount of component', () => { 192 | const mapRef = new Map('fake', {}) 193 | render(wrapWithAzureMapContext({ ...mapContextProps, mapRef }, {})) 194 | expect(mapContextProps.setMapReady).toHaveBeenCalledWith(true) 195 | }) 196 | 197 | it('should add props events to mapRef', () => { 198 | const mapRef = new Map('fake', { options: {} }) 199 | const dataCallback = () => { 200 | console.log('some fake text') 201 | } 202 | render( 203 | wrapWithAzureMapContext( 204 | { ...mapContextProps, mapRef }, 205 | { 206 | events: { 207 | data: dataCallback 208 | } 209 | } 210 | ) 211 | ) 212 | expect(mapRef.events.add).toHaveBeenCalledWith('ready', expect.any(Function)) 213 | expect(mapRef.events.add).toHaveBeenCalledWith('data', dataCallback) 214 | }) 215 | 216 | it('should render LoaderComponent if isMapReady is false and LoaderComponent exists', async () => { 217 | const mapRef = new Map('fake', { options: {} }) 218 | const { findByText } = render( 219 | wrapWithAzureMapContext( 220 | { ...mapContextProps, mapRef }, 221 | { 222 | LoaderComponent 223 | } 224 | ) 225 | ) 226 | const loaderElement = await findByText('Loader') 227 | expect(loaderElement).toMatchSnapshot() 228 | }) 229 | 230 | it('should create map with div and automatically generated id when if isMapReady is true and LoaderComponent exists', async () => { 231 | const mapRef = new Map('fake', { options: {} }) 232 | const { container } = render( 233 | wrapWithAzureMapContext({ ...mapContextProps, mapRef, isMapReady: true }, {}) 234 | ) 235 | expect(container).toMatchSnapshot() 236 | }) 237 | 238 | it('should render map with div and provvided id when if isMapReady is true and LoaderComponent exists', async () => { 239 | const mapRef = new Map('fake', { options: {} }) 240 | const { container } = render( 241 | wrapWithAzureMapContext( 242 | { ...mapContextProps, mapRef, isMapReady: true }, 243 | { 244 | LoaderComponent, 245 | providedMapId: 'some_fake_map_id' 246 | } 247 | ) 248 | ) 249 | expect(container).toMatchSnapshot() 250 | }) 251 | 252 | afterAll(() => { 253 | jest.unmock('./useCreateSprites') 254 | jest.unmock('./useCreateMapControls') 255 | jest.unmock('guid-typescript') 256 | }) 257 | }) 258 | -------------------------------------------------------------------------------- /src/components/AzureMap/AzureMap.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useContext, useEffect, useState, useRef } from 'react' 2 | import atlas, { Map } from 'azure-maps-control' 3 | import { IAzureMap, IAzureMapsContextProps, MapType } from '../../types' 4 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 5 | import { Guid } from 'guid-typescript' 6 | import { useCheckRef } from '../../hooks/useCheckRef' 7 | import { createImageSprites } from './useCreateSprites' 8 | import { createMapControls, createMapCustomControls } from './useCreateMapControls' 9 | 10 | const AzureMap = memo( 11 | ({ 12 | children, // @TODO We need to cover and type all possible childrens that we can pass to this component as child for. ex. Markers etc 13 | LoaderComponent = () =>
Loading ...
, 14 | providedMapId, 15 | containerClassName, 16 | styles, 17 | options = {}, 18 | imageSprites, 19 | controls, 20 | customControls, 21 | events, 22 | cameraOptions, 23 | trafficOptions, 24 | userInteraction, 25 | styleOptions, 26 | serviceOptions 27 | }: IAzureMap) => { 28 | const { 29 | setMapRef, 30 | removeMapRef, 31 | mapRef, 32 | setMapReady, 33 | isMapReady 34 | } = useContext(AzureMapsContext) 35 | const [mapId] = useState(providedMapId || Guid.create().toString()) 36 | const mapRefSource = useRef(null) 37 | useEffect(() => { 38 | if (mapRef) { 39 | mapRef.setTraffic(trafficOptions) 40 | } 41 | }, [trafficOptions]) 42 | 43 | useEffect(() => { 44 | if (mapRef) { 45 | mapRef.setUserInteraction(userInteraction) 46 | } 47 | }, [userInteraction]) 48 | 49 | useEffect(() => { 50 | if (mapRef) { 51 | mapRef.setCamera(cameraOptions) 52 | } 53 | }, [cameraOptions]) 54 | 55 | useEffect(() => { 56 | if (mapRef) { 57 | mapRef.setStyle(styleOptions) 58 | } 59 | }, [styleOptions]) 60 | 61 | useEffect(() => { 62 | if (mapRef && serviceOptions) { 63 | mapRef.setServiceOptions(serviceOptions) 64 | } 65 | }, [serviceOptions]) 66 | 67 | useCheckRef(mapRef, mapRef, (mref) => { 68 | mref.events.add('ready', () => { 69 | if (imageSprites) { 70 | createImageSprites(mref, imageSprites) 71 | } 72 | if (controls) { 73 | createMapControls(mref, controls) 74 | } 75 | if (customControls) { 76 | createMapCustomControls(mref, customControls) 77 | } 78 | if (trafficOptions) { 79 | mref.setTraffic(trafficOptions) 80 | } 81 | if (userInteraction) { 82 | mref.setUserInteraction(userInteraction) 83 | } 84 | if (cameraOptions) { 85 | mref.setCamera(cameraOptions) 86 | } 87 | if (styleOptions) { 88 | mref.setStyle(styleOptions) 89 | } 90 | if (serviceOptions) { 91 | mref.setServiceOptions(serviceOptions) 92 | } 93 | setMapReady(true) 94 | }) 95 | for (const eventType in events) { 96 | mref.events.add(eventType as any, events[eventType]) 97 | } 98 | }) 99 | 100 | useEffect(() => { 101 | if (mapRefSource.current === null) { 102 | mapRefSource.current = new atlas.Map(mapId, { 103 | ...(options || {}), 104 | // Assign default session ID with a prefix 105 | sessionId: options?.sessionId || `react-azure-maps:${Guid.create().toString()}` 106 | }) 107 | } 108 | setMapRef(mapRefSource.current) 109 | return () => { 110 | removeMapRef() 111 | } 112 | }, []) 113 | 114 | return ( 115 | <> 116 | {!isMapReady && LoaderComponent && } 117 |
118 | {isMapReady && children} 119 |
120 | 121 | ) 122 | } 123 | ) 124 | 125 | export default AzureMap 126 | -------------------------------------------------------------------------------- /src/components/AzureMap/__snapshots__/AzureMap.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AzureMap Component should create map with div and automatically generated id when if isMapReady is true and LoaderComponent exists 1`] = ` 4 |
5 |
9 |
10 | `; 11 | 12 | exports[`AzureMap Component should render LoaderComponent if isMapReady is false and LoaderComponent exists 1`] = ` 13 |
14 | Loader 15 |
16 | `; 17 | 18 | exports[`AzureMap Component should render map with div and provvided id when if isMapReady is true and LoaderComponent exists 1`] = ` 19 |
20 |
24 |
25 | `; 26 | -------------------------------------------------------------------------------- /src/components/AzureMap/useCreateMapControl.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import { createControl, createMapControls, createMapCustomControls } from './useCreateMapControls' 3 | import { Map } from 'azure-maps-control' 4 | import { IAzureMapControls, IAzureCustomControls } from '../../types' 5 | 6 | const fakeDefaultControls: IAzureMapControls[] = [ 7 | { 8 | controlName: 'CompassControl', 9 | options: { 10 | option1: 'option1-value' 11 | } 12 | }, 13 | { 14 | controlName: 'CompassControl', 15 | options: { 16 | option1: 'option1-value' 17 | } 18 | } 19 | ] 20 | 21 | const fakeCustomControlls: IAzureCustomControls[] = [ 22 | { 23 | control: { onAdd: jest.fn(), onRemove: jest.fn() }, 24 | controlOptions: {} 25 | } 26 | ] 27 | 28 | describe('Control hooks', () => { 29 | describe('useCreateMapControls tests', () => { 30 | it('should create two map controls and call proper method', () => { 31 | const mockMap = new Map('#fake-container', {}) 32 | mockMap.controls.add = jest.fn() 33 | renderHook(() => createMapControls(mockMap, fakeDefaultControls)) 34 | expect(mockMap.controls.add).toHaveBeenCalledWith( 35 | { compassOption: 'option' }, 36 | fakeDefaultControls[0].options 37 | ) 38 | expect(mockMap.controls.add).toHaveBeenCalledWith( 39 | { compassOption: 'option' }, 40 | fakeDefaultControls[1].options 41 | ) 42 | }) 43 | }) 44 | 45 | describe('createMapCustomControls tests', () => { 46 | it('should create custom map controls and call proper method', () => { 47 | const mockMap = new Map('#fake-container', {}) 48 | mockMap.controls.add = jest.fn() 49 | renderHook(() => createMapCustomControls(mockMap, fakeCustomControlls)) 50 | expect(mockMap.controls.add).toHaveBeenCalledTimes(1) 51 | expect(mockMap.controls.add).toHaveBeenCalledWith( 52 | fakeCustomControlls[0].control, 53 | fakeCustomControlls[0].controlOptions 54 | ) 55 | }) 56 | }) 57 | 58 | describe('createControl', () => { 59 | it('should return PitchControl if type equal PitchControl', () => { 60 | const createdControl = createControl('PitchControl', {}) 61 | expect(createdControl).toEqual({ pitchOption: 'option' }) 62 | }) 63 | 64 | it('should return ZoomControl if type equal StyleControl', () => { 65 | const createdControl = createControl('ZoomControl', {}) 66 | expect(createdControl).toEqual({ zoomOption: 'option' }) 67 | }) 68 | 69 | it('should return StyleControl if type equal StyleControl', () => { 70 | const createdControl = createControl('StyleControl', {}) 71 | expect(createdControl).toEqual({ styleOption: 'option' }) 72 | }) 73 | 74 | it('should return CompassControl if type equal CompassControl', () => { 75 | const createdControl = createControl('CompassControl', {}) 76 | expect(createdControl).toEqual({ compassOption: 'option' }) 77 | }) 78 | 79 | it('should return TrafficControl if type equal TrafficControl', () => { 80 | const createdControl = createControl('TrafficControl', {}) 81 | expect(createdControl).toEqual({ trafficOption: 'option' }) 82 | }) 83 | 84 | it('should return TrafficLegendControl if type equal TrafficLegendControl', () => { 85 | const createdControl = createControl('TrafficLegendControl', {}) 86 | expect(createdControl).toEqual({ trafficLegendOption: 'option' }) 87 | }) 88 | 89 | it('should return ScaleBarControl if type equal ScaleBarControl', () => { 90 | const createdControl = createControl('ScaleControl', {}) 91 | expect(createdControl).toEqual({ scaleOption: 'option' }) 92 | }) 93 | 94 | it('should return FullScreenControl if type equal FullScreenControl', () => { 95 | const createdControl = createControl('FullscreenControl', {}) 96 | expect(createdControl).toEqual({ fullscreenOption: 'option' }) 97 | }) 98 | 99 | it('should return undefined if there is no control with type', () => { 100 | const createdControl = createControl('SomeOtherType', {}) 101 | expect(createdControl).toEqual(undefined) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/components/AzureMap/useCreateMapControls.tsx: -------------------------------------------------------------------------------- 1 | import { IAzureCustomControls, IAzureMapControls, MapType } from '../../types' 2 | import atlas, { 3 | CompassControlOptions, 4 | ControlOptions, 5 | PitchControlOptions, 6 | StyleControlOptions, 7 | TrafficControlOptions, 8 | ZoomControlOptions, 9 | ScaleControlOptions, 10 | FullscreenControlOptions 11 | } from 'azure-maps-control' 12 | 13 | export const createMapControls = (mapRef: MapType, controls: IAzureMapControls[]) => { 14 | controls.forEach((control: IAzureMapControls) => { 15 | const { controlName, options, controlOptions } = control 16 | mapRef.controls.add( 17 | createControl(controlName, controlOptions) as atlas.control.ControlBase, 18 | options as ControlOptions 19 | ) 20 | }) 21 | } 22 | 23 | export const createControl = ( 24 | type: string, 25 | options?: ControlOptions 26 | ): atlas.control.ControlBase | undefined => { 27 | switch (type) { 28 | case 'CompassControl': 29 | return new atlas.control.CompassControl(options as CompassControlOptions) 30 | case 'PitchControl': 31 | return new atlas.control.PitchControl(options as PitchControlOptions) 32 | case 'StyleControl': 33 | return new atlas.control.StyleControl(options as StyleControlOptions) 34 | case 'ZoomControl': 35 | return new atlas.control.ZoomControl(options as ZoomControlOptions) 36 | case 'TrafficControl': 37 | return new atlas.control.TrafficControl(options as TrafficControlOptions) 38 | case 'TrafficLegendControl': 39 | return new atlas.control.TrafficLegendControl() 40 | case 'ScaleControl': 41 | return new atlas.control.ScaleControl(options as ScaleControlOptions) 42 | case 'FullscreenControl': 43 | return new atlas.control.FullscreenControl(options as FullscreenControlOptions) 44 | default: 45 | console.warn('Check the type and passed props properties or try CustomControl') 46 | } 47 | } 48 | 49 | export const createMapCustomControls = ( 50 | mapRef: MapType, 51 | customControls: IAzureCustomControls[] 52 | ) => { 53 | customControls.forEach(({ control, controlOptions }: IAzureCustomControls) => { 54 | mapRef.controls.add(control, controlOptions) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/AzureMap/useCreateSprites.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import { createImageSprites } from './useCreateSprites' 3 | import { Map } from 'azure-maps-control' 4 | 5 | describe('createImageSprites tests', () => { 6 | it('should create image sprintes with icon field and call proper methods', async () => { 7 | const mockMap = new Map('#fake-container', {}) 8 | const fakeImageSprite = { 9 | id: 'id', 10 | icon: 'icon', 11 | templateName: 'template', 12 | color: 'color', 13 | secondaryColor: 'color', 14 | scale: 1 15 | } 16 | const { result } = renderHook(() => createImageSprites(mockMap, [fakeImageSprite])) 17 | await result.current 18 | expect(mockMap.imageSprite.add).toHaveBeenCalledWith('id', 'icon') 19 | expect(mockMap.imageSprite.createFromTemplate).toHaveBeenCalledWith( 20 | 'id', 21 | 'template', 22 | 'color', 23 | 'color', 24 | 1 25 | ) 26 | }) 27 | 28 | it('should create image sprintes with no icon field and not call imageSprite.add', async () => { 29 | const mockMap = new Map('#fake-container', {}) 30 | const fakeImageSprite = { 31 | id: 'id', 32 | templateName: 'template', 33 | color: 'color', 34 | secondaryColor: 'color', 35 | scale: 1 36 | } 37 | const { result } = renderHook(() => createImageSprites(mockMap, [fakeImageSprite])) 38 | await result.current 39 | expect(mockMap.imageSprite.add).not.toHaveBeenCalled() 40 | expect(mockMap.imageSprite.createFromTemplate).toHaveBeenCalledWith( 41 | 'id', 42 | 'template', 43 | 'color', 44 | 'color', 45 | 1 46 | ) 47 | }) 48 | 49 | it('should create image sprintes with no icon and not call imageSprite.add with default template', async () => { 50 | const mockMap = new Map('#fake-container', {}) 51 | const fakeImageSprite = { 52 | id: 'id', 53 | color: 'color', 54 | secondaryColor: 'color', 55 | scale: 1 56 | } 57 | const { result } = renderHook(() => createImageSprites(mockMap, [fakeImageSprite])) 58 | await result.current 59 | expect(mockMap.imageSprite.add).not.toHaveBeenCalled() 60 | expect(mockMap.imageSprite.createFromTemplate).toHaveBeenCalledWith( 61 | 'id', 62 | 'marker', 63 | 'color', 64 | 'color', 65 | 1 66 | ) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/components/AzureMap/useCreateSprites.tsx: -------------------------------------------------------------------------------- 1 | import { IAzureMapImageSprite, MapType } from '../../types' 2 | 3 | export const createImageSprites = async ( 4 | mapRef: MapType, 5 | spriteArray: IAzureMapImageSprite[] 6 | ) => { 7 | await Promise.all( 8 | spriteArray.map( 9 | async ({ id, templateName, color, secondaryColor, scale, icon }: IAzureMapImageSprite) => { 10 | if (icon) { 11 | // Add an icon image to the map's image sprite for use with symbols and patterns. 12 | await mapRef.imageSprite.add(id, icon) 13 | } 14 | // Creates and adds an image to the maps image sprite. Reference this in the Polygon or Symbol layer. 15 | return mapRef.imageSprite.createFromTemplate( 16 | id, 17 | templateName || 'marker', 18 | color, 19 | secondaryColor, 20 | scale 21 | ) 22 | } 23 | ) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/AzureMapFeature.test.tsx: -------------------------------------------------------------------------------- 1 | import { Map, source } from 'azure-maps-control' 2 | import React from 'react' 3 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 4 | import { AzureMapDataSourceContext, IAzureMapFeature } from '../../react-azure-maps' 5 | import { render } from '@testing-library/react' 6 | import AzureMapFeature from './AzureMapFeature' 7 | import { useFeature } from './useFeature' 8 | import atlas from 'azure-maps-control' 9 | 10 | jest.mock('./useFeature') 11 | jest.mock('./useCreateAzureMapFeature.ts', () => ({ 12 | createAzureMapFeature: () => ({}) 13 | })) 14 | 15 | const mapRef = new Map('fake', {}) 16 | const dataSourceRef = new source.DataSource() 17 | const mapContextProps = { 18 | mapRef: null, 19 | isMapReady: false, 20 | setMapReady: jest.fn(), 21 | removeMapRef: jest.fn(), 22 | setMapRef: jest.fn() 23 | } 24 | 25 | const wrapWithAzureMapContexts = (featureProps: IAzureMapFeature) => { 26 | return ( 27 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | describe('AzureMapFeature tests', () => { 40 | it('should create AzureMapFeature', () => { 41 | const featureProps: IAzureMapFeature = { type: 'LineString' } 42 | render(wrapWithAzureMapContexts(featureProps)) 43 | expect(useFeature).toHaveBeenCalled() 44 | }) 45 | 46 | it('should create feature', () => { 47 | const featureProps: IAzureMapFeature = { 48 | type: 'LineString', 49 | variant: 'feature', 50 | id: 'id', 51 | properties: { prop: 'prop' } 52 | } 53 | // @ts-ignore 54 | atlas.data.Feature.mockClear() 55 | render(wrapWithAzureMapContexts(featureProps)) 56 | expect(atlas.data.Feature).toHaveBeenCalledWith({}, { prop: 'prop' }, 'id') 57 | }) 58 | 59 | it('should create shape', () => { 60 | const featureProps: IAzureMapFeature = { 61 | type: 'LineString', 62 | variant: 'shape', 63 | id: 'id', 64 | properties: { prop: 'prop' } 65 | } 66 | // @ts-ignore 67 | atlas.Shape.mockClear() 68 | render(wrapWithAzureMapContexts(featureProps)) 69 | expect(atlas.Shape).toHaveBeenCalledWith({}, 'id', { prop: 'prop' }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/AzureMapFeature.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useContext, useEffect } from 'react' 2 | import atlas from 'azure-maps-control' 3 | 4 | import { FeatureType, IAzureMapDataSourceProps, IAzureMapFeature, ShapeType } from '../../types' 5 | import { AzureMapDataSourceContext } from '../../contexts/AzureMapDataSourceContext' 6 | import { createAzureMapFeature } from './useCreateAzureMapFeature' 7 | import { useFeature } from './useFeature' 8 | 9 | const AzureMapFeature = memo((props: IAzureMapFeature) => { 10 | const { properties, id, variant = 'feature' } = props 11 | const { dataSourceRef } = useContext(AzureMapDataSourceContext) 12 | const [featureRef, setFeatureRef] = React.useState(null) 13 | const [shapeRef, setShapeRef] = React.useState(null) 14 | // Hook for proper handling shapes and features 15 | useFeature(props, dataSourceRef, shapeRef, featureRef) 16 | 17 | useEffect(() => { 18 | const featureSource: atlas.data.Geometry | undefined = createAzureMapFeature(props) 19 | 20 | if ((!featureRef || !shapeRef) && featureSource) { 21 | switch (variant) { 22 | case 'shape': 23 | setShapeRef(new atlas.Shape(featureSource, id, properties)) 24 | break 25 | case 'feature': 26 | setFeatureRef(new atlas.data.Feature(featureSource, properties, id)) 27 | break 28 | } 29 | } 30 | }, []) 31 | 32 | return null 33 | }) 34 | 35 | export default AzureMapFeature 36 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/useCreateAzureMapFeature.test.tsx: -------------------------------------------------------------------------------- 1 | import atlas from 'azure-maps-control' 2 | import { IAzureMapFeature } from '../../types' 3 | import { createAzureMapFeature } from './useCreateAzureMapFeature' 4 | 5 | const fakeCoordinate: atlas.data.Position = new atlas.data.Position(10, 10) 6 | const fakeCoordinates: atlas.data.Position[] = [ 7 | new atlas.data.Position(10, 10), 8 | new atlas.data.Position(20, 20) 9 | ] 10 | const fakeMultipleCoordinates: Array> = [ 11 | [new atlas.data.Position(10, 10)], 12 | [new atlas.data.Position(20, 20)] 13 | ] 14 | const fakeMultipleDimensionCoordinates: Array>> = [ 15 | [[new atlas.data.Position(10, 10)], [new atlas.data.Position(20, 20)]] 16 | ] 17 | const fakeBbox: atlas.data.BoundingBox = new atlas.data.BoundingBox( 18 | new atlas.data.Position(10, 10), 19 | new atlas.data.Position(20, 20) 20 | ) 21 | 22 | describe('AzureMapFeature hooks', () => { 23 | describe('createAzureMapFeature tests', () => { 24 | it('should return Point if type equal Point', () => { 25 | const pointProps: IAzureMapFeature = { 26 | type: 'Point', 27 | coordinate: fakeCoordinate 28 | } 29 | const createPoint = createAzureMapFeature(pointProps) 30 | expect(createPoint).toEqual({ coords: [10, 10], type: 'Point' }) 31 | }) 32 | it('should return MultiPoint if type equal MultiPoint', () => { 33 | const multiPointProps: IAzureMapFeature = { 34 | type: 'MultiPoint', 35 | coordinates: fakeCoordinates, 36 | bbox: fakeBbox 37 | } 38 | const createMultiPoint = createAzureMapFeature(multiPointProps) 39 | expect(createMultiPoint).toEqual({ 40 | bbox: [ 41 | [10, 10], 42 | [20, 20] 43 | ], 44 | coords: [ 45 | [10, 10], 46 | [20, 20] 47 | ], 48 | type: 'MultiPoint' 49 | }) 50 | }) 51 | it('should return LineString if type equal LineString', () => { 52 | const lineStringProps: IAzureMapFeature = { 53 | type: 'LineString', 54 | coordinates: fakeCoordinates, 55 | bbox: fakeBbox 56 | } 57 | const createLineString = createAzureMapFeature(lineStringProps) 58 | expect(createLineString).toEqual({ 59 | bbox: [ 60 | [10, 10], 61 | [20, 20] 62 | ], 63 | coords: [ 64 | [10, 10], 65 | [20, 20] 66 | ], 67 | type: 'LineString' 68 | }) 69 | }) 70 | it('should return MultiLineString if type equal MultiLineString', () => { 71 | const multiLineStringProps: IAzureMapFeature = { 72 | type: 'MultiLineString', 73 | multipleCoordinates: fakeMultipleCoordinates, 74 | bbox: fakeBbox 75 | } 76 | const createMultiLineStringProps = createAzureMapFeature(multiLineStringProps) 77 | expect(createMultiLineStringProps).toEqual({ 78 | bbox: [ 79 | [10, 10], 80 | [20, 20] 81 | ], 82 | multipleCoordinates: [[[10, 10]], [[20, 20]]], 83 | type: 'MultiLineString' 84 | }) 85 | }) 86 | it('should return Polygon if type equal Polygon', () => { 87 | const lineStringProps: IAzureMapFeature = { 88 | type: 'Polygon', 89 | coordinates: fakeCoordinates, 90 | bbox: fakeBbox 91 | } 92 | const createLineString = createAzureMapFeature(lineStringProps) 93 | expect(createLineString).toEqual({ 94 | bbox: [ 95 | [10, 10], 96 | [20, 20] 97 | ], 98 | coords: [ 99 | [10, 10], 100 | [20, 20] 101 | ], 102 | type: 'Polygon' 103 | }) 104 | }) 105 | it('should return MultiPolygon if type equal MultiPolygon', () => { 106 | const multiPolygonProps: IAzureMapFeature = { 107 | type: 'MultiPolygon', 108 | multipleDimensionCoordinates: fakeMultipleDimensionCoordinates, 109 | bbox: fakeBbox 110 | } 111 | const createMultiPolygon = createAzureMapFeature(multiPolygonProps) 112 | expect(createMultiPolygon).toEqual({ 113 | bbox: [ 114 | [10, 10], 115 | [20, 20] 116 | ], 117 | type: 'MultiPolygon', 118 | multipleDimensionCoordinates: [[[[10, 10]], [[20, 20]]]] 119 | }) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/useCreateAzureMapFeature.ts: -------------------------------------------------------------------------------- 1 | import { IAzureMapFeature } from '../../types' 2 | import atlas from 'azure-maps-control' 3 | 4 | export const createAzureMapFeature = ({ 5 | type, 6 | coordinate, 7 | coordinates, 8 | multipleCoordinates, 9 | multipleDimensionCoordinates, 10 | bbox 11 | }: IAzureMapFeature): atlas.data.Geometry | undefined => { 12 | switch (type) { 13 | case 'Point': 14 | return coordinate && new atlas.data.Point(coordinate) 15 | case 'MultiPoint': 16 | return coordinates && new atlas.data.MultiPoint(coordinates, bbox) 17 | case 'LineString': 18 | return coordinates && new atlas.data.LineString(coordinates, bbox) 19 | case 'MultiLineString': 20 | return multipleCoordinates && new atlas.data.MultiLineString(multipleCoordinates, bbox) 21 | case 'Polygon': 22 | return coordinates && new atlas.data.Polygon(coordinates, bbox) 23 | case 'MultiPolygon': 24 | return ( 25 | multipleDimensionCoordinates && 26 | new atlas.data.MultiPolygon(multipleDimensionCoordinates, bbox) 27 | ) 28 | default: 29 | console.warn('Check the type and passed props properties') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/useFeature.test.tsx: -------------------------------------------------------------------------------- 1 | import atlas, { data, source } from 'azure-maps-control' 2 | import { useFeature } from './useFeature' 3 | import { IAzureMapFeature } from '../../types' 4 | import { renderHook } from '@testing-library/react' 5 | 6 | const fakePosition = new data.Point(new data.Position(0, 0)) 7 | 8 | const dataSourceRef = new source.DataSource() 9 | const featureRef = new data.Feature(fakePosition) 10 | const shapeRef = new atlas.Shape(fakePosition) 11 | const fakeShapeProps: IAzureMapFeature = { 12 | setProperties: jest.fn(), 13 | setCoords: new atlas.data.Position(10, 10), 14 | type: 'Point' 15 | } 16 | const fakeProps: IAzureMapFeature = { 17 | type: 'Point' 18 | } 19 | 20 | describe('useFeature tests', () => { 21 | it('should add feature to dataRef', () => { 22 | renderHook(() => useFeature(fakeProps, dataSourceRef, null, featureRef)) 23 | expect(dataSourceRef.add).toHaveBeenCalled() 24 | }) 25 | it('should add shape to dataRef', () => { 26 | renderHook(() => useFeature(fakeProps, dataSourceRef, shapeRef, null)) 27 | expect(dataSourceRef.add).toHaveBeenCalled() 28 | }) 29 | it('should add shape and setProperties', () => { 30 | renderHook(() => useFeature(fakeShapeProps, dataSourceRef, shapeRef, null)) 31 | expect(dataSourceRef.add).toHaveBeenCalled() 32 | expect(shapeRef.setCoordinates).toHaveBeenCalled() 33 | expect(shapeRef.setProperties).toHaveBeenCalled() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/AzureMapFeature/useFeature.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { DataSourceType, IAzureMapFeature, ShapeType, FeatureType } from '../../types' 3 | import { useCheckRef } from '../../hooks/useCheckRef' 4 | import atlas from 'azure-maps-control' 5 | 6 | export const useFeature = ( 7 | { setCoords, setProperties }: IAzureMapFeature, 8 | dataSourceRef: DataSourceType | null, 9 | shapeRef: ShapeType | null, 10 | featureRef: FeatureType | null 11 | ) => { 12 | // Simple feature's usecases and methods 13 | useCheckRef(dataSourceRef, featureRef, (dref, fref) => { 14 | if(dref instanceof atlas.source.DataSource){ 15 | dref.add(fref) 16 | return () => { 17 | dref.remove(fref) 18 | } 19 | } else if (dataSourceRef instanceof atlas.source.VectorTileSource) { 20 | console.error(`Unable to add Feature(${fref.id}) to VectorTileSource(${dataSourceRef.getId()}): AzureMapFeature has to be a child of AzureMapDataSourceProvider`) 21 | } 22 | }) 23 | 24 | // Shape's usecases and methods 25 | useCheckRef(dataSourceRef, shapeRef, (dref, sref) => { 26 | if(dref instanceof atlas.source.DataSource){ 27 | dref.add(sref) 28 | return () => { 29 | dref.remove(sref) 30 | } 31 | } else if (dataSourceRef instanceof atlas.source.VectorTileSource) { 32 | console.error(`Unable to add Shape(${sref.getId()}) to VectorTileSource(${dataSourceRef.getId()}): AzureMapFeature has to be a child of AzureMapDataSourceProvider`) 33 | } 34 | }) 35 | 36 | useEffect(() => { 37 | if (shapeRef && setCoords) { 38 | shapeRef.setCoordinates(setCoords) 39 | } 40 | }, [setCoords]) 41 | 42 | useEffect(() => { 43 | if (shapeRef && setProperties) { 44 | shapeRef.setProperties(setProperties) 45 | } 46 | }, [setProperties]) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/AzureMapMarkers/AzureMapHtmlMarker/AzureMapHtmlMarker.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { Map, Popup } from 'azure-maps-control' 3 | import React from 'react' 4 | import { AzureMapsContext } from '../../../contexts/AzureMapContext' 5 | import AzureMapHtmlMarker from './AzureMapHtmlMarker' 6 | import { IAzureMapHtmlMarker } from '../../../types' 7 | import atlas from 'azure-maps-control' 8 | 9 | const mapContextProps = { 10 | mapRef: null, 11 | isMapReady: false, 12 | setMapReady: jest.fn(), 13 | removeMapRef: jest.fn(), 14 | setMapRef: jest.fn() 15 | } 16 | const mapRef = new Map('fake', {}) 17 | 18 | const wrapWithAzureMapContext = (props: IAzureMapHtmlMarker) => { 19 | return ( 20 | 26 | 27 | 28 | ) 29 | } 30 | 31 | describe('AzureMaphtmlMarker tests', () => { 32 | it('should create html marker without error and add it to map ref', () => { 33 | mapRef.markers.add = jest.fn() 34 | const { container, unmount } = render(wrapWithAzureMapContext({})) 35 | expect(mapRef.markers.add).toHaveBeenCalled() 36 | expect(container).toMatchSnapshot() 37 | unmount() 38 | expect(mapRef.markers.remove).toHaveBeenCalled() 39 | }) 40 | 41 | it('should remove marker from map ref', () => { 42 | mapRef.markers.remove = jest.fn() 43 | const { unmount } = render(wrapWithAzureMapContext({})) 44 | unmount() 45 | expect(mapRef.markers.remove).toHaveBeenCalled() 46 | }) 47 | 48 | it('should add events for marker to map', () => { 49 | mapRef.events.add = jest.fn() 50 | render( 51 | wrapWithAzureMapContext({ 52 | events: [ 53 | { 54 | eventName: 'click', 55 | callback: jest.fn() 56 | } 57 | ] 58 | }) 59 | ) 60 | expect(mapRef.events.add).toHaveBeenCalledWith( 61 | 'click', 62 | expect.any(Object), 63 | expect.any(Function) 64 | ) 65 | }) 66 | describe('options and content change', () => { 67 | const markerRef = new atlas.HtmlMarker() 68 | // We need somehow to override current mock constructor ts currently do not allow that 69 | // @ts-ignore 70 | atlas.HtmlMarker = jest.fn(() => markerRef) 71 | 72 | it('should call setOptions on markerRef', () => { 73 | markerRef.setOptions = jest.fn() 74 | render(wrapWithAzureMapContext({ options: { option: 'option' } })) 75 | expect(markerRef.setOptions).toHaveBeenCalledWith({ option: 'option' }) 76 | }) 77 | 78 | it('should call setOptions on markerRef when marketContent prop is passed', () => { 79 | markerRef.setOptions = jest.fn() 80 | render(wrapWithAzureMapContext({ markerContent:
})) 81 | expect(markerRef.setOptions).toHaveBeenCalledWith({ htmlContent: '
' }) 82 | }) 83 | 84 | it('should open marker popup', () => { 85 | render(wrapWithAzureMapContext({ isPopupVisible: true, markerContent:
})) 86 | // Currently we only check if getOptions get called @TODO 87 | expect(markerRef.getOptions).toHaveBeenCalled() 88 | }) 89 | 90 | it('should close marker popup', () => { 91 | render(wrapWithAzureMapContext({ isPopupVisible: false, markerContent:
})) 92 | // Currently we only check if getOptions get called @TODO 93 | expect(markerRef.getOptions).toHaveBeenCalled() 94 | }) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /src/components/AzureMapMarkers/AzureMapHtmlMarker/AzureMapHtmlMarker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect, memo } from 'react' 2 | import { renderToStaticMarkup } from 'react-dom/server' 3 | import atlas from 'azure-maps-control' 4 | 5 | import { IAzureMapsContextProps, IAzureMapHtmlMarker, MapType } from '../../../types' 6 | import { AzureMapsContext } from '../../../contexts/AzureMapContext' 7 | import { useCheckRefMount } from '../../../hooks/useCheckRef' 8 | 9 | const AzureMapHtmlMarker = memo( 10 | ({ markerContent, options, events, isPopupVisible }: IAzureMapHtmlMarker) => { 11 | const [markerRef] = useState( 12 | new atlas.HtmlMarker({ 13 | ...options, 14 | htmlContent: markerContent && renderToStaticMarkup(markerContent) 15 | }) 16 | ) 17 | const { mapRef } = useContext(AzureMapsContext) 18 | 19 | useCheckRefMount(mapRef, true, (mref) => { 20 | mref.markers.add(markerRef) 21 | events && 22 | events.forEach(({ eventName, callback }) => { 23 | mref.events.add(eventName, markerRef, callback) 24 | }) 25 | return () => { 26 | mref.markers.remove(markerRef) 27 | } 28 | }) 29 | 30 | useEffect(() => { 31 | if (markerRef && mapRef) { 32 | markerRef.setOptions({ 33 | ...options, 34 | htmlContent: markerContent && renderToStaticMarkup(markerContent) 35 | }) 36 | } 37 | }, [markerContent, options]) 38 | 39 | useEffect(() => { 40 | if (markerRef && markerRef.getOptions().popup && mapRef) { 41 | const popupRef = markerRef.getOptions().popup 42 | if (isPopupVisible) { 43 | popupRef?.setOptions({ 44 | position: markerRef.getOptions().position 45 | }) 46 | popupRef?.open(mapRef) 47 | } else { 48 | popupRef?.close() 49 | } 50 | } 51 | }, [isPopupVisible, options, mapRef]) 52 | 53 | return null 54 | } 55 | ) 56 | 57 | export default AzureMapHtmlMarker 58 | -------------------------------------------------------------------------------- /src/components/AzureMapMarkers/AzureMapHtmlMarker/__snapshots__/AzureMapHtmlMarker.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AzureMaphtmlMarker tests should create html marker without error and add it to map ref 1`] = `
`; 4 | -------------------------------------------------------------------------------- /src/components/AzureMapPopup/AzureMapPopup.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { Map, Popup } from 'azure-maps-control' 3 | import React from 'react' 4 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 5 | import AzureMapPopup from './AzureMapPopup' 6 | import { IAzureMapPopup } from '../../types' 7 | 8 | const mapContextProps = { 9 | mapRef: null, 10 | isMapReady: false, 11 | setMapReady: jest.fn(), 12 | removeMapRef: jest.fn(), 13 | setMapRef: jest.fn() 14 | } 15 | const mapRef = new Map('fake', {}) 16 | mapRef.events.add = jest.fn() 17 | const popupRef = new Popup() 18 | 19 | jest.mock('./useCreateAzureMapPopup.ts', () => ({ useCreatePopup: () => popupRef })) 20 | 21 | const wrapWithAzureMapContext = (props: IAzureMapPopup) => { 22 | return ( 23 | 29 | 30 | 31 | ) 32 | } 33 | 34 | describe('AzureMapPopup tests', () => { 35 | it('should create popup and add events on mount', () => { 36 | render( 37 | wrapWithAzureMapContext({ 38 | events: [ 39 | { 40 | eventName: 'drag', 41 | callback: () => {} 42 | } 43 | ], 44 | popupContent:
45 | }) 46 | ) 47 | expect(mapRef.events.add).toHaveBeenCalledWith('drag', popupRef, expect.any(Function)) 48 | }) 49 | 50 | it('should remove popup on unmount', () => { 51 | const { unmount } = render( 52 | wrapWithAzureMapContext({ 53 | popupContent:
54 | }) 55 | ) 56 | unmount() 57 | expect(mapRef.popups.remove).toHaveBeenCalled() 58 | }) 59 | 60 | it('should open popup when isVisible is true and isOpen returns false', () => { 61 | popupRef.isOpen = jest.fn(() => false) 62 | render( 63 | wrapWithAzureMapContext({ 64 | popupContent:
, 65 | isVisible: true 66 | }) 67 | ) 68 | expect(popupRef.open).toHaveBeenCalledWith(mapRef) 69 | }) 70 | 71 | it('should close popup when isVisible is false and isOpen returns true', () => { 72 | popupRef.isOpen = jest.fn(() => true) 73 | mapRef.popups.getPopups = jest.fn(() => [popupRef]) 74 | render( 75 | wrapWithAzureMapContext({ 76 | popupContent:
, 77 | isVisible: false 78 | }) 79 | ) 80 | expect(popupRef.close).toHaveBeenCalled() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/components/AzureMapPopup/AzureMapPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, memo } from 'react' 2 | 3 | import { IAzureMapsContextProps, MapType, IAzureMapPopup } from '../../types' 4 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 5 | import { useCheckRefMount } from '../../hooks/useCheckRef' 6 | import { useCreatePopup } from './useCreateAzureMapPopup' 7 | 8 | const AzureMapPopup = memo(({ isVisible, popupContent, options, events }: IAzureMapPopup) => { 9 | const { mapRef } = useContext(AzureMapsContext) 10 | const popupRef = useCreatePopup({ options, popupContent, isVisible }) 11 | 12 | useCheckRefMount(mapRef, true, mref => { 13 | events && 14 | events.forEach(({ eventName, callback }) => { 15 | mref.events.add(eventName, popupRef, callback) 16 | }) 17 | return () => { 18 | mref.popups.remove(popupRef) 19 | } 20 | }) 21 | 22 | useEffect(() => { 23 | if (mapRef) { 24 | if (isVisible && !popupRef.isOpen()) { 25 | popupRef.open(mapRef) 26 | } else if (mapRef.popups.getPopups().length && !isVisible && popupRef.isOpen()) { 27 | popupRef.close() 28 | } 29 | } 30 | }, [isVisible]) 31 | 32 | return null 33 | }) 34 | 35 | export default AzureMapPopup 36 | -------------------------------------------------------------------------------- /src/components/AzureMapPopup/useCreateAzureMapPopup.test.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { renderHook } from '@testing-library/react' 3 | import React from 'react' 4 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 5 | import { useCreatePopup } from './useCreateAzureMapPopup' 6 | import { MapType } from '../../types' 7 | 8 | const mapContextProps = { 9 | isMapReady: false, 10 | setMapReady: jest.fn(), 11 | removeMapRef: jest.fn(), 12 | setMapRef: jest.fn() 13 | } 14 | const mapRef = { 15 | events: { 16 | addOnce: jest.fn() 17 | }, 18 | popups: { 19 | add: jest.fn() 20 | } 21 | } 22 | 23 | const wrapWithAzureMapContext = ({ children }: { children?: ReactNode | null }) => { 24 | return ( 25 | 31 | {children} 32 | 33 | ) 34 | } 35 | 36 | describe('useCreatePopup tests', () => { 37 | it('should create popup with options', () => { 38 | const popupProps = { 39 | options: {}, 40 | popupContent:
, 41 | isVisible: false 42 | } 43 | const { result } = renderHook(() => useCreatePopup(popupProps), { 44 | wrapper: wrapWithAzureMapContext 45 | }) 46 | expect(result.current.setOptions).toHaveBeenCalled() 47 | expect(result.current).toEqual({ 48 | setOptions: expect.any(Function), 49 | isOpen: expect.any(Function), 50 | open: expect.any(Function), 51 | close: expect.any(Function) 52 | }) 53 | }) 54 | 55 | it('should create popup with options and open it if isVisible is true', () => { 56 | const popupProps = { 57 | options: {}, 58 | popupContent:
, 59 | isVisible: true 60 | } 61 | const { result } = renderHook(() => useCreatePopup(popupProps), { 62 | wrapper: wrapWithAzureMapContext 63 | }) 64 | expect(result.current.open).toHaveBeenCalledWith(mapRef) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/AzureMapPopup/useCreateAzureMapPopup.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from 'react' 2 | import { renderToStaticMarkup } from 'react-dom/server' 3 | import atlas from 'azure-maps-control' 4 | 5 | import { IAzureMapPopup, IAzureMapsContextProps, MapType } from '../../types' 6 | import { AzureMapsContext } from '../../contexts/AzureMapContext' 7 | import { useCheckRef } from '../../hooks/useCheckRef' 8 | export const useCreatePopup = ({ 9 | options, 10 | popupContent, 11 | isVisible 12 | }: Pick) => { 13 | const [popupRef] = useState( 14 | new atlas.Popup({ ...options, content: renderToStaticMarkup(popupContent) }) 15 | ) 16 | const { mapRef } = useContext(AzureMapsContext) 17 | 18 | useCheckRef(mapRef, mapRef, mref => { 19 | mref.events.addOnce('ready', () => mref.popups.add(popupRef)) 20 | }) 21 | 22 | useEffect(() => { 23 | popupRef.setOptions({ 24 | ...options, 25 | content: renderToStaticMarkup(popupContent) 26 | }) 27 | if (mapRef && isVisible && !popupRef.isOpen()) { 28 | popupRef.open(mapRef) 29 | } 30 | }, [options, popupContent]) 31 | 32 | return popupRef 33 | } 34 | 35 | export default useCreatePopup 36 | -------------------------------------------------------------------------------- /src/components/helpers/mapHelper.test.ts: -------------------------------------------------------------------------------- 1 | import { generateLinesFromArrayOfPosition, generatePixelHeading } from './mapHelper' 2 | import atlas from 'azure-maps-control' 3 | 4 | atlas.Pixel.getHeading = jest.fn(() => 0) 5 | 6 | describe('generateLinesFromArrayOfPosition', () => { 7 | it('should call generateLinesFromArrayOfPosition without error', () => { 8 | const result = generateLinesFromArrayOfPosition([new atlas.data.Position(0, 1)]) 9 | expect(result).toEqual({ 10 | bbox: undefined, 11 | coords: [[0, 1]], 12 | type: 'LineString' 13 | }) 14 | }) 15 | }) 16 | 17 | describe('generatePixelHeading', () => { 18 | it('should call generatePixelHeading without error', () => { 19 | const result = generatePixelHeading(new atlas.Pixel(0, 1), new atlas.Pixel(0, 1)) 20 | expect(result).toEqual(0) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/helpers/mapHelper.ts: -------------------------------------------------------------------------------- 1 | import atlas from 'azure-maps-control' 2 | import { DataSourceType, MapType } from '../../types' 3 | 4 | export const generateLinesFromArrayOfPosition = ( 5 | coordinates: atlas.data.Position[] 6 | ): atlas.data.LineString => { 7 | const line = new atlas.data.LineString(coordinates) 8 | return line 9 | } 10 | 11 | export const generatePixelHeading = (origin: atlas.Pixel, destination: atlas.Pixel) => { 12 | const heading = atlas.Pixel.getHeading(origin, destination) 13 | return heading 14 | } 15 | 16 | export const getLayersDependingOnDatasource = (mref: MapType, dst: DataSourceType) => { 17 | return mref.layers.getLayers().filter((l) => { 18 | if ((l as atlas.layer.SymbolLayer).getSource) { 19 | const sourceLayer = (l as atlas.layer.SymbolLayer).getSource() 20 | const dsId = typeof sourceLayer === 'string' ? sourceLayer : sourceLayer.getId() 21 | return dsId === dst.getId() 22 | } 23 | return false 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/contexts/AzureMapContext.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { renderHook } from '@testing-library/react' 3 | import { act } from 'react-dom/test-utils' 4 | import { Map } from 'azure-maps-control' 5 | import { AzureMapsContext, AzureMapsProvider } from '../contexts/AzureMapContext' 6 | 7 | const mapRef = new Map('fake', {}) 8 | 9 | const useContextConsumer = () => { 10 | const mapContext = useContext(AzureMapsContext) 11 | return mapContext 12 | } 13 | 14 | const wrapDataWithAzureMapsContext = ({ children }: { children?: any }) => { 15 | return {children} 16 | } 17 | 18 | describe('AzureMapDataSourceProvider tests', () => { 19 | it('should add data source to the map ref on mount', async () => { 20 | mapRef.sources.add = jest.fn() 21 | const { result } = renderHook(() => useContextConsumer(), { 22 | wrapper: wrapDataWithAzureMapsContext 23 | }) 24 | act(() => { 25 | result.current.setMapRef(mapRef) 26 | result.current.setMapReady(true) 27 | }) 28 | expect(result.current.mapRef).toEqual(mapRef) 29 | }) 30 | 31 | it('should clear on removeMapRef', async () => { 32 | mapRef.sources.add = jest.fn() 33 | const { result } = renderHook(() => useContextConsumer(), { 34 | wrapper: wrapDataWithAzureMapsContext 35 | }) 36 | result.current.removeMapRef() 37 | expect(result.current.mapRef).toEqual(null) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/contexts/AzureMapContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactElement, useState ,useContext} from 'react' 2 | import { Map } from 'azure-maps-control' 3 | import { IAzureMap, IAzureMapsContextProps } from '../types' 4 | 5 | export const AzureMapsContext = createContext({ 6 | mapRef: null, 7 | isMapReady: false, 8 | setMapRef: (mapRef: Map) => {}, 9 | removeMapRef: () => {}, 10 | setMapReady: () => {} 11 | }) 12 | const { Provider, Consumer: AzureMapsConsumer } = AzureMapsContext 13 | 14 | type IAzureMapsStatefulProviderProps = { 15 | children?: ReactElement 16 | } 17 | 18 | const AzureMapsStatefulProvider = ({ children }: IAzureMapsStatefulProviderProps) => { 19 | const [mapRef, setMapRef] = useState(null) 20 | const [isMapReady, setIsMapReady] = useState(false) 21 | 22 | return ( 23 | setMapRef(null) 30 | }} 31 | > 32 | {children} 33 | 34 | ) 35 | } 36 | 37 | const useAzureMaps = () => useContext(AzureMapsContext) 38 | 39 | export { AzureMapsConsumer, AzureMapsStatefulProvider as AzureMapsProvider, useAzureMaps } 40 | -------------------------------------------------------------------------------- /src/contexts/AzureMapDataSourceContext.test.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { renderHook } from '@testing-library/react' 3 | import atlas, { layer, Map } from 'azure-maps-control' 4 | import React from 'react' 5 | import { AzureMapsContext } from '../contexts/AzureMapContext' 6 | import { 7 | AzureMapDataSourceProvider, 8 | AzureMapDataSourceContext 9 | } from '../contexts/AzureMapDataSourceContext' 10 | import { IAzureDataSourceStatefulProviderProps } from '../types' 11 | 12 | const mapContextProps = { 13 | mapRef: null, 14 | isMapReady: false, 15 | setMapReady: jest.fn(), 16 | removeMapRef: jest.fn(), 17 | setMapRef: jest.fn() 18 | } 19 | const mapRef = new Map('fake', {}) 20 | 21 | const useContextConsumer = () => { 22 | const dataSourceContext = useContext(AzureMapDataSourceContext) 23 | return dataSourceContext 24 | } 25 | 26 | const wrapWithDataSourceContext = (props: IAzureDataSourceStatefulProviderProps) => ({ 27 | children 28 | }: { 29 | children?: any 30 | }) => { 31 | return ( 32 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | describe('AzureMapDataSourceProvider tests', () => { 44 | it('should add data source to the map ref on mount', () => { 45 | mapRef.sources.add = jest.fn() 46 | const { result } = renderHook(() => useContextConsumer(), { 47 | wrapper: wrapWithDataSourceContext({ id: 'id' }) 48 | }) 49 | expect(mapRef.sources.add).toHaveBeenCalledWith(result.current.dataSourceRef) 50 | }) 51 | 52 | it('should add event to data source', () => { 53 | mapRef.events.add = jest.fn() 54 | renderHook(() => useContextConsumer(), { 55 | wrapper: wrapWithDataSourceContext({ id: 'id', events: { render: () => {} } }) 56 | }) 57 | expect(mapRef.events.add).toHaveBeenCalledWith( 58 | 'render', 59 | expect.any(Object), 60 | expect.any(Function) 61 | ) 62 | }) 63 | 64 | it('should call importDataFromUrl if dataFromUrl was not falsy', () => { 65 | const { result } = renderHook(() => useContextConsumer(), { 66 | wrapper: wrapWithDataSourceContext({ id: 'id', dataFromUrl: 'dataFromUrl' }) 67 | }) 68 | expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) 69 | expect( 70 | (result.current.dataSourceRef as atlas.source.DataSource).importDataFromUrl 71 | ).toHaveBeenCalledWith('dataFromUrl') 72 | }) 73 | 74 | it('should call add collection if collection was not falsy', () => { 75 | const { result } = renderHook(() => useContextConsumer(), { 76 | wrapper: wrapWithDataSourceContext({ id: 'id', collection: [] }) 77 | }) 78 | expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) 79 | const dataSourceRef = result.current.dataSourceRef as atlas.source.DataSource 80 | expect(dataSourceRef.add).toHaveBeenCalledWith([]) 81 | expect(dataSourceRef.clear).toHaveBeenCalledWith() 82 | }) 83 | 84 | it('should call add collection and clear method if collection was changed', () => { 85 | const { result, rerender } = renderHook(() => useContextConsumer(), { 86 | wrapper: wrapWithDataSourceContext({ id: 'id', collection: [] }) 87 | }) 88 | rerender({}) 89 | expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) 90 | const dataSourceRef = result.current.dataSourceRef as atlas.source.DataSource 91 | expect(dataSourceRef.add).toHaveBeenCalledTimes(2) 92 | expect(dataSourceRef.clear).toHaveBeenCalledTimes(1) 93 | }) 94 | 95 | it('should call setOptions and clear method if options was changed', () => { 96 | const { result } = renderHook(() => useContextConsumer(), { 97 | wrapper: wrapWithDataSourceContext({ id: 'id', options: { option: 'option' } }) 98 | }) 99 | expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) 100 | expect( 101 | (result.current.dataSourceRef as atlas.source.DataSource).setOptions 102 | ).toHaveBeenLastCalledWith({ option: 'option' }) 103 | }) 104 | 105 | it('should remove data source from the map ref on unmount', () => { 106 | mapRef.events.remove = jest.fn() 107 | const events = { render: () => {} } 108 | const { unmount, result } = renderHook(() => useContextConsumer(), { 109 | wrapper: wrapWithDataSourceContext({ id: 'id', options: { option: 'option' }, events }) 110 | }) 111 | 112 | unmount() 113 | 114 | expect(mapRef.sources.remove).toHaveBeenCalledWith(result.current.dataSourceRef) 115 | expect(mapRef.events.remove).toHaveBeenCalledWith( 116 | 'render', 117 | result.current.dataSourceRef, 118 | events.render 119 | ) 120 | }) 121 | 122 | it('should remove all layers that are using the same datasource from the map ref on unmount', () => { 123 | const dsToBeRemoved = new atlas.source.DataSource('ds_to_be_removed') 124 | const dsToKeep = new atlas.source.DataSource('ds_to_keep') 125 | 126 | const symbolLayer = new layer.SymbolLayer(dsToBeRemoved, 'layer_to_be_removed') 127 | const bubbleLayer = new layer.BubbleLayer(dsToKeep, 'layer_to_keep') 128 | 129 | symbolLayer.getSource = jest.fn(() => dsToBeRemoved) 130 | 131 | mapRef.layers.getLayers = jest.fn(() => [symbolLayer, bubbleLayer]) 132 | 133 | const { unmount } = renderHook(() => useContextConsumer(), { 134 | wrapper: wrapWithDataSourceContext({ id: dsToBeRemoved.getId() }) 135 | }) 136 | 137 | unmount() 138 | expect(mapRef.layers.remove).toHaveBeenCalledTimes(1) 139 | expect(mapRef.layers.remove).toHaveBeenNthCalledWith(1, 'layer_to_be_removed') 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /src/contexts/AzureMapDataSourceContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react' 2 | import { 3 | DataSourceType, 4 | IAzureDataSourceStatefulProviderProps, 5 | IAzureMapDataSourceProps, 6 | IAzureMapsContextProps, 7 | MapType 8 | } from '../types' 9 | import atlas from 'azure-maps-control' 10 | import { AzureMapsContext } from './AzureMapContext' 11 | import { useCheckRef } from '../hooks/useCheckRef' 12 | import { getLayersDependingOnDatasource } from '../components/helpers/mapHelper' 13 | 14 | const AzureMapDataSourceContext = createContext({ 15 | dataSourceRef: null 16 | }) 17 | const { Provider, Consumer: AzureMapDataSourceConsumer } = AzureMapDataSourceContext 18 | 19 | /** 20 | * @param id 21 | * @param children 22 | * @param options 23 | * @param events 24 | * @param dataFromUrl Async Data from URL 25 | * @param collection Use for contain Feature Collection !All Feature child will be ignored when collection value will change 26 | */ 27 | const AzureMapDataSourceStatefulProvider = ({ 28 | id, 29 | children, 30 | options, 31 | events, 32 | dataFromUrl, 33 | collection 34 | }: IAzureDataSourceStatefulProviderProps) => { 35 | const [dataSourceRef] = useState( 36 | new atlas.source.DataSource(id, options) 37 | ) 38 | const { mapRef } = useContext(AzureMapsContext) 39 | useCheckRef(mapRef, dataSourceRef, (mref, dref) => { 40 | for (const eventType in events || {}) { 41 | mref.events.add(eventType as any, dref, events[eventType]) 42 | } 43 | mref.sources.add(dref) 44 | if (dref instanceof atlas.source.DataSource) { 45 | if (dataFromUrl) { 46 | dref.importDataFromUrl(dataFromUrl) 47 | } 48 | if (collection) { 49 | dref.add(collection) 50 | } 51 | } 52 | return () => { 53 | for (const eventType in events || {}) { 54 | mref.events.remove(eventType as any, dref, events[eventType]) 55 | } 56 | 57 | getLayersDependingOnDatasource(mref, dref).forEach((l) => { 58 | mref.layers.remove(l.getId() ? l.getId() : l) 59 | }) 60 | mref.sources.remove(dref) 61 | } 62 | }) 63 | 64 | useEffect(() => { 65 | if (dataSourceRef && collection) { 66 | // Cleared old values prevent duplication 67 | dataSourceRef.clear() 68 | dataSourceRef.add(collection) 69 | } 70 | }, [collection]) 71 | 72 | useEffect(() => { 73 | if (dataSourceRef && options) { 74 | dataSourceRef.setOptions(options) 75 | } 76 | }, [options]) 77 | 78 | return ( 79 | 84 | {mapRef && children} 85 | 86 | ) 87 | } 88 | 89 | export { 90 | AzureMapDataSourceContext, 91 | AzureMapDataSourceConsumer, 92 | AzureMapDataSourceStatefulProvider as AzureMapDataSourceProvider, 93 | Provider as AzureMapDataSourceRawProvider 94 | } 95 | -------------------------------------------------------------------------------- /src/contexts/AzureMapLayerContext.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, act } from '@testing-library/react' 3 | import { AzureMapLayerProvider } from './AzureMapLayerContext' 4 | 5 | describe('AzureMapLayerProvider', () => { 6 | it('should create and render AzureMapLayerProvider', () => { 7 | const { container } = render() 8 | expect(container).toMatchSnapshot() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/contexts/AzureMapLayerContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import { IAzureLayerStatefulProviderProps, IAzureMapLayerProps } from '../types' 3 | import { useAzureMapLayer } from '../hooks/useAzureMapLayer' 4 | 5 | const AzureMapLayerContext = createContext({ 6 | layerRef: null 7 | }) 8 | const { Provider, Consumer: AzureMapLayerConsumer } = AzureMapLayerContext 9 | 10 | const AzureMapLayerStatefulProvider = ({ 11 | id, 12 | options, 13 | type, 14 | events, 15 | lifecycleEvents, 16 | onCreateCustomLayer 17 | }: IAzureLayerStatefulProviderProps) => { 18 | const { layerRef } = useAzureMapLayer({ 19 | id, 20 | options, 21 | type, 22 | events, 23 | lifecycleEvents, 24 | onCreateCustomLayer 25 | }) 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | export { 36 | AzureMapLayerContext, 37 | AzureMapLayerConsumer, 38 | AzureMapLayerStatefulProvider as AzureMapLayerProvider 39 | } 40 | -------------------------------------------------------------------------------- /src/contexts/AzureMapVectorTileSourceProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import React, { useContext } from 'react' 3 | import atlas, { layer, Map } from 'azure-maps-control' 4 | import { IAzureVectorTileSourceStatefulProviderProps } from '../types' 5 | import { AzureMapsContext } from './AzureMapContext' 6 | import { AzureMapVectorTileSourceProvider } from './AzureMapVectorTileSourceProvider' 7 | import { AzureMapDataSourceContext } from '../contexts/AzureMapDataSourceContext' 8 | 9 | const mapContextProps = { 10 | mapRef: null, 11 | isMapReady: false, 12 | setMapReady: jest.fn(), 13 | removeMapRef: jest.fn(), 14 | setMapRef: jest.fn() 15 | } 16 | const mapRef = new Map('fake', {}) 17 | 18 | const useContextConsumer = () => { 19 | const dataSourceContext = useContext(AzureMapDataSourceContext) 20 | return dataSourceContext 21 | } 22 | 23 | const wrapWithVectorTileSourceContext = (props: IAzureVectorTileSourceStatefulProviderProps) => ({ 24 | children 25 | }: { 26 | children?: any 27 | }) => { 28 | return ( 29 | 35 | 36 | {children} 37 | 38 | 39 | ) 40 | } 41 | 42 | describe('AzureMapVectorTileSourceProvider tests', () => { 43 | it('should create data source with passed id and options', () => { 44 | const { result } = renderHook(() => useContextConsumer(), { 45 | wrapper: wrapWithVectorTileSourceContext({ id: 'id', options: { minZoom: 0, maxZoom: 12 } }) 46 | }) 47 | 48 | expect(result.current.dataSourceRef?.getId()).toEqual('id') 49 | expect(result.current.dataSourceRef?.getOptions()).toEqual({ minZoom: 0, maxZoom: 12 }) 50 | }) 51 | 52 | it('should add data source to the map ref on mount', () => { 53 | mapRef.sources.add = jest.fn() 54 | const { result } = renderHook(() => useContextConsumer(), { 55 | wrapper: wrapWithVectorTileSourceContext({ id: 'id' }) 56 | }) 57 | expect(mapRef.sources.add).toHaveBeenCalledWith(result.current.dataSourceRef) 58 | }) 59 | 60 | it('should add event to data source', () => { 61 | mapRef.events.add = jest.fn() 62 | renderHook(() => useContextConsumer(), { 63 | wrapper: wrapWithVectorTileSourceContext({ 64 | id: 'id', 65 | events: { sourceadded: (source) => {} } 66 | }) 67 | }) 68 | expect(mapRef.events.add).toHaveBeenCalledWith( 69 | 'sourceadded', 70 | expect.any(Object), 71 | expect.any(Function) 72 | ) 73 | }) 74 | 75 | it('should remove data source from the map ref on unmount', () => { 76 | mapRef.events.remove = jest.fn() 77 | const events = { sourceadded: () => {} } 78 | const { unmount, result } = renderHook(() => useContextConsumer(), { 79 | wrapper: wrapWithVectorTileSourceContext({ id: 'id', options: { option: 'option' }, events }) 80 | }) 81 | 82 | unmount() 83 | 84 | expect(mapRef.sources.remove).toHaveBeenCalledWith(result.current.dataSourceRef) 85 | expect(mapRef.events.remove).toHaveBeenCalledWith( 86 | 'sourceadded', 87 | result.current.dataSourceRef, 88 | events.sourceadded 89 | ) 90 | }) 91 | 92 | it('should remove all layers that are using the same datasource from the map ref on unmount', () => { 93 | const dsToBeRemoved = new atlas.source.DataSource('ds_to_be_removed') 94 | const dsToKeep = new atlas.source.DataSource('ds_to_keep') 95 | 96 | const symbolLayer = new layer.SymbolLayer(dsToBeRemoved, 'layer_to_be_removed') 97 | const bubbleLayer = new layer.BubbleLayer(dsToKeep, 'layer_to_keep') 98 | 99 | symbolLayer.getSource = jest.fn(() => dsToBeRemoved) 100 | 101 | mapRef.layers.getLayers = jest.fn(() => [symbolLayer, bubbleLayer]) 102 | 103 | const { unmount } = renderHook(() => useContextConsumer(), { 104 | wrapper: wrapWithVectorTileSourceContext({ id: dsToBeRemoved.getId() }) 105 | }) 106 | 107 | unmount() 108 | expect(mapRef.layers.remove).toHaveBeenCalledTimes(1) 109 | expect(mapRef.layers.remove).toHaveBeenNthCalledWith(1, 'layer_to_be_removed') 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /src/contexts/AzureMapVectorTileSourceProvider.tsx: -------------------------------------------------------------------------------- 1 | import atlas from 'azure-maps-control' 2 | import React, { useContext, useState } from 'react' 3 | import { useCheckRef } from '../hooks/useCheckRef' 4 | import { 5 | DataSourceType, 6 | IAzureMapsContextProps, 7 | IAzureMapSourceEventType, 8 | IAzureVectorTileSourceStatefulProviderProps, 9 | MapType 10 | } from '../types' 11 | import { AzureMapDataSourceRawProvider as Provider } from './AzureMapDataSourceContext' 12 | import { AzureMapsContext } from './AzureMapContext' 13 | import { getLayersDependingOnDatasource } from '../components/helpers/mapHelper' 14 | 15 | /** 16 | * @param id datasource identifier 17 | * @param children layer providers representing datasource layers 18 | * @param options vector tile datasource options: see atlas.VectorTileSourceOptions 19 | * @param events a object containing sourceadded, sourceremoved event handlers 20 | */ 21 | const AzureMapVectorTileSourceStatefulProvider = ({ 22 | id, 23 | children, 24 | options, 25 | events = {} 26 | }: IAzureVectorTileSourceStatefulProviderProps) => { 27 | const [dataSourceRef] = useState( 28 | new atlas.source.VectorTileSource(id, options) 29 | ) 30 | const { mapRef } = useContext(AzureMapsContext) 31 | useCheckRef(mapRef, dataSourceRef, (mref, dref) => { 32 | for (const eventType in events) { 33 | const handler = events[eventType as IAzureMapSourceEventType] as ( 34 | e: atlas.source.Source 35 | ) => void | undefined 36 | if (handler) { 37 | mref.events.add(eventType as IAzureMapSourceEventType, dref, handler) 38 | } 39 | } 40 | mref.sources.add(dref) 41 | 42 | return () => { 43 | for (const eventType in events || {}) { 44 | const handler = events[eventType as IAzureMapSourceEventType] as ( 45 | e: atlas.source.Source 46 | ) => void | undefined 47 | if (handler) { 48 | mref.events.remove(eventType as IAzureMapSourceEventType, dref, handler) 49 | } 50 | } 51 | 52 | getLayersDependingOnDatasource(mref, dref).forEach((l) => { 53 | mref.layers.remove(l.getId() ? l.getId() : l) 54 | }) 55 | mref.sources.remove(dref) 56 | } 57 | }) 58 | 59 | return ( 60 | 65 | {mapRef && children} 66 | 67 | ) 68 | } 69 | 70 | export { AzureMapVectorTileSourceStatefulProvider as AzureMapVectorTileSourceProvider } 71 | -------------------------------------------------------------------------------- /src/contexts/__snapshots__/AzureMapLayerContext.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AzureMapLayerProvider should create and render AzureMapLayerProvider 1`] = `
`; 4 | -------------------------------------------------------------------------------- /src/hooks/constructLayer.test.tsx: -------------------------------------------------------------------------------- 1 | import atlas from 'azure-maps-control' 2 | import { constructLayer } from './useAzureMapLayer' 3 | import { IAzureMapLayerType } from '../types' 4 | 5 | describe('constructLayer', () => { 6 | const datasourceRef = {} as atlas.source.DataSource 7 | it('should return SymbolLayer props if type equal to SymbolLayer', () => { 8 | const createLayer = constructLayer( 9 | { 10 | id: 'SymbolLayerId', 11 | options: {}, 12 | type: 'SymbolLayer' 13 | }, 14 | datasourceRef 15 | ) 16 | expect(createLayer).toEqual({ 17 | layer: 'SymbolLayer', 18 | options: {}, 19 | id: 'SymbolLayerId', 20 | datasourceRef, 21 | setOptions: expect.any(Function), 22 | getId: expect.any(Function) 23 | }) 24 | }) 25 | it('should return HeatLayer props if type equal to HeatLayer', () => { 26 | const createLayer = constructLayer( 27 | { 28 | id: 'HeatLayerId', 29 | options: {}, 30 | type: 'HeatLayer' 31 | }, 32 | datasourceRef 33 | ) 34 | expect(createLayer).toEqual({ 35 | layer: 'HeatLayer', 36 | options: {}, 37 | id: 'HeatLayerId', 38 | datasourceRef 39 | }) 40 | }) 41 | it('should return ImageLayer props if type equal to ImageLayer', () => { 42 | const createLayer = constructLayer( 43 | { 44 | id: 'imageLayerId', 45 | options: {}, 46 | type: 'ImageLayer' 47 | }, 48 | datasourceRef 49 | ) 50 | expect(createLayer).toEqual({ layer: 'ImageLayer', options: {}, id: 'imageLayerId' }) 51 | }) 52 | it('should return LineLayer props if type equal to LineLayer', () => { 53 | const createLayer = constructLayer( 54 | { 55 | id: 'LineLayerId', 56 | options: {}, 57 | type: 'LineLayer' 58 | }, 59 | datasourceRef 60 | ) 61 | expect(createLayer).toEqual({ 62 | layer: 'LineLayer', 63 | options: {}, 64 | id: 'LineLayerId', 65 | datasourceRef 66 | }) 67 | }) 68 | it('should return PolygonExtrusionLayer props if type equal to PolygonExtrusionLayer', () => { 69 | const createLayer = constructLayer( 70 | { 71 | id: 'PolygonExtrusionLayerId', 72 | options: {}, 73 | type: 'PolygonExtrusionLayer' 74 | }, 75 | datasourceRef 76 | ) 77 | expect(createLayer).toEqual({ 78 | layer: 'PolygonExtrusionLayer', 79 | options: {}, 80 | id: 'PolygonExtrusionLayerId', 81 | datasourceRef 82 | }) 83 | }) 84 | it('should return PolygonLayer props if type equal to PolygonLayer', () => { 85 | const createLayer = constructLayer( 86 | { 87 | id: 'PolygonLayerId', 88 | options: {}, 89 | type: 'PolygonLayer' 90 | }, 91 | datasourceRef 92 | ) 93 | expect(createLayer).toEqual({ 94 | layer: 'PolygonLayer', 95 | options: {}, 96 | id: 'PolygonLayerId', 97 | datasourceRef 98 | }) 99 | }) 100 | it('should return TileLayer props if type equal to TileLayer', () => { 101 | const createLayer = constructLayer( 102 | { 103 | id: 'TileLayerId', 104 | options: {}, 105 | type: 'TileLayer' 106 | }, 107 | datasourceRef 108 | ) 109 | expect(createLayer).toEqual({ layer: 'TileLayer', options: {}, id: 'TileLayerId' }) 110 | }) 111 | it('should return BubbleLayer props if type equal to BubbleLayer', () => { 112 | const createLayer = constructLayer( 113 | { 114 | id: 'BubbleLayerId', 115 | options: {}, 116 | type: 'BubbleLayer' 117 | }, 118 | datasourceRef 119 | ) 120 | expect(createLayer).toEqual({ 121 | layer: 'BubbleLayer', 122 | options: {}, 123 | id: 'BubbleLayerId', 124 | datasourceRef 125 | }) 126 | }) 127 | 128 | it('should return null if type is not equal to any type', () => { 129 | const createLayer = constructLayer( 130 | { 131 | id: 'id', 132 | options: {}, 133 | type: '' as IAzureMapLayerType 134 | }, 135 | datasourceRef 136 | ) 137 | expect(createLayer).toEqual(null) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /src/hooks/useAzureMapLayer.test.tsx: -------------------------------------------------------------------------------- 1 | import { source, layer } from 'azure-maps-control' 2 | import { ReactNode } from 'react' 3 | import { renderHook } from '@testing-library/react' 4 | import { useAzureMapLayer } from './useAzureMapLayer' 5 | import { Map } from 'azure-maps-control' 6 | import React from 'react' 7 | import { AzureMapsContext } from '../contexts/AzureMapContext' 8 | import { AzureMapDataSourceContext } from '../contexts/AzureMapDataSourceContext' 9 | import { IAzureLayerStatefulProviderProps } from '../types' 10 | 11 | const mapContextProps = { 12 | mapRef: null, 13 | isMapReady: false, 14 | setMapReady: jest.fn(), 15 | removeMapRef: jest.fn(), 16 | setMapRef: jest.fn() 17 | } 18 | const mapRef = new Map('fake', {}) 19 | mapRef.layers.getLayerById = jest.fn().mockImplementation(() => null) 20 | 21 | const wrapWithAzureMapContext = ({ children }: { children?: ReactNode | null }) => { 22 | const datasourceRef = {} as source.DataSource 23 | return ( 24 | 30 | 35 | {children} 36 | 37 | 38 | ) 39 | } 40 | 41 | describe('useAzureMapLayer tests', () => { 42 | it('should create custom layer on callback', () => { 43 | const customLayerRef = { getId: jest.fn() } 44 | const useAzureMapLayerProps: IAzureLayerStatefulProviderProps = { 45 | type: 'custom', 46 | // We need to pas as any because of LayerEvents 47 | onCreateCustomLayer: jest.fn((dref, mref) => customLayerRef as any) 48 | } 49 | const { result } = renderHook(() => useAzureMapLayer(useAzureMapLayerProps), { 50 | wrapper: wrapWithAzureMapContext 51 | }) 52 | expect(useAzureMapLayerProps.onCreateCustomLayer).toHaveBeenCalled() 53 | expect(result.current.layerRef).toEqual(customLayerRef) 54 | }) 55 | 56 | it('should create standard layer and set ref', () => { 57 | const { result } = renderHook( 58 | () => 59 | useAzureMapLayer({ 60 | type: 'SymbolLayer', 61 | id: 'id', 62 | options: { option1: 'option1' } 63 | }), 64 | { wrapper: wrapWithAzureMapContext } 65 | ) 66 | expect(result.current.layerRef).toEqual({ 67 | datasourceRef: { 68 | option1: 'option1' 69 | }, 70 | getId: expect.any(Function), 71 | id: 'id', 72 | layer: 'SymbolLayer', 73 | options: {}, 74 | setOptions: expect.any(Function) 75 | }) 76 | }) 77 | 78 | it('should handle add events to layerRef', () => { 79 | mapRef.events.add = jest.fn() 80 | const events = { click: () => {} } 81 | renderHook( 82 | () => 83 | useAzureMapLayer({ 84 | type: 'SymbolLayer', 85 | id: 'id', 86 | events 87 | }), 88 | { wrapper: wrapWithAzureMapContext } 89 | ) 90 | expect(mapRef.events.add).toHaveBeenCalledWith('click', expect.any(Object), events.click) 91 | }) 92 | 93 | it('should handle add lifeCycleEvents to layerRef', () => { 94 | mapRef.events.add = jest.fn() 95 | const lifecycleEvents = { onCreate: () => {} } 96 | renderHook( 97 | () => 98 | useAzureMapLayer({ 99 | type: 'SymbolLayer', 100 | id: 'id', 101 | lifecycleEvents 102 | }), 103 | { wrapper: wrapWithAzureMapContext } 104 | ) 105 | expect(mapRef.events.add).toHaveBeenCalledWith( 106 | 'onCreate', 107 | expect.any(Object), 108 | lifecycleEvents.onCreate 109 | ) 110 | }) 111 | 112 | it('shouldRemove layer from map on unmoun', () => { 113 | const symbolLayer = {} as layer.SymbolLayer 114 | mapRef.layers.getLayerById = jest.fn().mockImplementation(() => symbolLayer) 115 | mapRef.layers.remove = jest.fn() 116 | const { unmount } = renderHook( 117 | () => 118 | useAzureMapLayer({ 119 | type: 'SymbolLayer' 120 | }), 121 | { wrapper: wrapWithAzureMapContext } 122 | ) 123 | unmount() 124 | expect(mapRef.layers.remove).toHaveBeenCalledWith(expect.any(Object)) 125 | }) 126 | 127 | it('should update options on change and call setOptions on layerRef', () => { 128 | const { result, rerender } = renderHook( 129 | () => 130 | useAzureMapLayer({ 131 | type: 'SymbolLayer', 132 | options: { 133 | option: 'option' 134 | } 135 | }), 136 | { wrapper: wrapWithAzureMapContext } 137 | ) 138 | 139 | rerender({ 140 | options: { 141 | newOption: 'new' 142 | } 143 | }) 144 | expect(result.current.layerRef?.setOptions).toHaveBeenCalledTimes(2) 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /src/hooks/useAzureMapLayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react' 2 | import { 3 | IAzureLayerStatefulProviderProps, 4 | IAzureMapDataSourceProps, 5 | IAzureMapsContextProps, 6 | LayerType, 7 | DataSourceType 8 | } from '../types' 9 | import atlas from 'azure-maps-control' 10 | import { AzureMapsContext } from '../contexts/AzureMapContext' 11 | import { AzureMapDataSourceContext } from '../contexts/AzureMapDataSourceContext' 12 | import { useCheckRef } from './useCheckRef' 13 | import { MapType } from '../types' 14 | 15 | export const constructLayer = ( 16 | { id, options = {}, type }: Omit, 17 | dataSourceRef: DataSourceType 18 | ) => { 19 | switch (type) { 20 | case 'SymbolLayer': 21 | return new atlas.layer.SymbolLayer(dataSourceRef, id, options) 22 | case 'HeatLayer': 23 | return new atlas.layer.HeatMapLayer(dataSourceRef, id, options) 24 | case 'ImageLayer': 25 | return new atlas.layer.ImageLayer(options, id) 26 | case 'LineLayer': 27 | return new atlas.layer.LineLayer(dataSourceRef, id, options) 28 | case 'PolygonExtrusionLayer': 29 | return new atlas.layer.PolygonExtrusionLayer(dataSourceRef, id, options) 30 | case 'PolygonLayer': 31 | return new atlas.layer.PolygonLayer(dataSourceRef, id, options) 32 | case 'TileLayer': 33 | return new atlas.layer.TileLayer(options, id) 34 | case 'BubbleLayer': 35 | return new atlas.layer.BubbleLayer(dataSourceRef, id, options) 36 | default: 37 | return null 38 | } 39 | } 40 | 41 | export const useAzureMapLayer = ({ 42 | id, 43 | options, 44 | type, 45 | events, 46 | lifecycleEvents, 47 | onCreateCustomLayer 48 | }: IAzureLayerStatefulProviderProps) => { 49 | const { mapRef } = useContext(AzureMapsContext) 50 | const { dataSourceRef } = useContext(AzureMapDataSourceContext) 51 | const [layerRef, setLayerRef] = useState(null) 52 | 53 | useCheckRef(!layerRef, dataSourceRef, (...[, ref]) => { 54 | let layer = null 55 | if (type === 'custom') { 56 | layer = onCreateCustomLayer && onCreateCustomLayer(ref, mapRef) 57 | } else { 58 | layer = constructLayer({ id, options, type }, ref) 59 | } 60 | setLayerRef(layer as LayerType) 61 | }) 62 | 63 | useCheckRef(mapRef, layerRef, (mref, lref) => { 64 | for (const eventType in events) { 65 | mref.events.add(eventType as any, lref, events[eventType]) 66 | } 67 | for (const event in lifecycleEvents) { 68 | mref.events.add(event as any, lref, lifecycleEvents[event]) 69 | } 70 | mref.layers.add(lref) 71 | return () => { 72 | try { 73 | if (mref.layers.getLayerById(lref.getId())) { 74 | mref.layers.remove(lref.getId() ? lref.getId() : lref) 75 | } 76 | } catch (e) { 77 | console.error('Error on remove layer', e) 78 | } 79 | } 80 | }) 81 | 82 | useEffect(() => { 83 | if (layerRef && options) { 84 | layerRef.setOptions(options) 85 | } 86 | }, [options]) 87 | 88 | return { 89 | layerRef 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/hooks/useCheckRef.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import { useCheckRef, useCheckRefMount } from './useCheckRef' 3 | 4 | describe('useCheckRef tests', () => { 5 | it('should make proper checks with on ref change and run callback if both deps are not falsy', async () => { 6 | const callback = jest.fn((depr, onr) => {}) 7 | renderHook(() => useCheckRef(true, true, callback)) 8 | expect(callback).toHaveBeenCalled() 9 | }) 10 | 11 | it('should make proper checks with on ref change and not run callback if one of deps is falsy', async () => { 12 | const callback = jest.fn((depr, onr) => {}) 13 | renderHook(() => useCheckRef(true, false, callback)) 14 | expect(callback).not.toHaveBeenCalled() 15 | }) 16 | }) 17 | 18 | describe('useCheckRefMount tests', () => { 19 | it('should make proper checks on mount and run callback if both deps are not falsy', async () => { 20 | const callback = jest.fn((depr, onr) => {}) 21 | renderHook(() => useCheckRefMount(true, true, callback)) 22 | expect(callback).toHaveBeenCalled() 23 | }) 24 | 25 | it('should make proper checks on mount and not run callback if one of deps is falsy', async () => { 26 | const callback = jest.fn((depr, onr) => {}) 27 | renderHook(() => useCheckRefMount(true, false, callback)) 28 | expect(callback).not.toHaveBeenCalled() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/hooks/useCheckRef.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export function useCheckRef( 4 | dep: T | null, 5 | on: T1 | null, 6 | callback: (dep: T, on: T1) => void 7 | ) { 8 | useEffect(() => { 9 | if (dep && on) { 10 | return callback(dep, on) 11 | } 12 | }, [on]) 13 | } 14 | 15 | export function useCheckRefMount( 16 | dep: T | null, 17 | on: T1 | null, 18 | callback: (dep: T, on: T1) => void 19 | ) { 20 | useEffect(() => { 21 | if (dep && on) { 22 | return callback(dep, on) 23 | } 24 | }, []) 25 | } 26 | -------------------------------------------------------------------------------- /src/react-azure-maps.ts: -------------------------------------------------------------------------------- 1 | export { default as AzureMap } from './components/AzureMap/AzureMap' 2 | export { default as AzureMapHtmlMarker } from './components/AzureMapMarkers/AzureMapHtmlMarker/AzureMapHtmlMarker' 3 | export { default as AzureMapFeature } from './components/AzureMapFeature/AzureMapFeature' 4 | export { 5 | AzureMapsContext, 6 | AzureMapsConsumer, 7 | AzureMapsProvider, 8 | useAzureMaps 9 | } from './contexts/AzureMapContext' 10 | export { 11 | AzureMapDataSourceContext, 12 | AzureMapDataSourceConsumer, 13 | AzureMapDataSourceProvider, 14 | } from './contexts/AzureMapDataSourceContext' 15 | export { AzureMapVectorTileSourceProvider } from './contexts/AzureMapVectorTileSourceProvider' 16 | export { 17 | AzureMapLayerContext, 18 | AzureMapLayerConsumer, 19 | AzureMapLayerProvider 20 | } from './contexts/AzureMapLayerContext' 21 | export { default as AzureMapPopup } from './components/AzureMapPopup/AzureMapPopup' 22 | export { default as useCreatePopup } from './components/AzureMapPopup/useCreateAzureMapPopup' 23 | 24 | export { 25 | generateLinesFromArrayOfPosition, 26 | generatePixelHeading 27 | } from './components/helpers/mapHelper' 28 | 29 | export * from './types' 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass, CSSProperties, ReactElement, ReactNode, FunctionComponent } from 'react' 2 | import atlas, { 3 | AnimationOptions, 4 | CameraBoundsOptions, 5 | CameraOptions, 6 | DataSourceOptions, 7 | HeatMapLayerOptions, 8 | HtmlMarker, 9 | HtmlMarkerEvents, 10 | HtmlMarkerOptions, 11 | ImageLayerOptions, 12 | LineLayerOptions, 13 | Map, 14 | MapDataEvent, 15 | MapErrorEvent, 16 | MapEvent, 17 | MapMouseEvent, 18 | MapMouseWheelEvent, 19 | MapTouchEvent, 20 | Options, 21 | PolygonExtrusionLayerOptions, 22 | PolygonLayerOptions, 23 | PopupEvents, 24 | PopupOptions, 25 | ServiceOptions, 26 | Shape, 27 | StyleOptions, 28 | SymbolLayerOptions, 29 | TargetedEvent, 30 | TileLayerOptions, 31 | TrafficOptions, 32 | UserInteractionOptions, 33 | Control, 34 | BubbleLayerOptions, 35 | LayerOptions, 36 | VectorTileSourceOptions 37 | } from 'azure-maps-control' 38 | 39 | export type IAzureMapOptions = ServiceOptions & 40 | StyleOptions & 41 | UserInteractionOptions & 42 | (CameraOptions | CameraBoundsOptions) 43 | 44 | export type IAzureMapChildren = 45 | | ReactElement 46 | | ReactElement 47 | | ReactElement 48 | 49 | export type IAzureMap = { 50 | children?: Array | IAzureMapChildren 51 | providedMapId?: string 52 | containerClassName?: string 53 | styles?: CSSProperties 54 | LoaderComponent?: ComponentClass | FunctionComponent 55 | options?: IAzureMapOptions 56 | imageSprites?: IAzureMapImageSprite[] 57 | controls?: IAzureMapControls[] 58 | customControls?: IAzureCustomControls[] 59 | events?: IAzureMapEvent | any 60 | cameraOptions?: AzureSetCameraOptions 61 | trafficOptions?: TrafficOptions 62 | userInteraction?: UserInteractionOptions 63 | styleOptions?: StyleOptions 64 | serviceOptions?: ServiceOptions 65 | } 66 | export type IAzureCustomControls = { 67 | control: Control 68 | controlOptions?: ControlOptions 69 | } 70 | 71 | export type IAzureMapControls = { 72 | controlName: string 73 | controlOptions?: Options 74 | options?: ControlOptions | undefined 75 | } 76 | 77 | export type IAzureMapImageSprite = { 78 | id: string 79 | templateName?: string 80 | color?: string 81 | secondaryColor?: string 82 | scale?: number 83 | icon?: string | HTMLImageElement | ImageData 84 | } 85 | 86 | export type IAzureMapContextState = { 87 | mapRef: Map | null 88 | isMapReady: boolean | false 89 | setMapRef(mapRef: Map): void 90 | removeMapRef(): void 91 | setMapReady(isMapReady: boolean): void 92 | } 93 | 94 | export type IAzureMapHtmlMarkerEvent = { 95 | eventName: keyof HtmlMarkerEvents 96 | callback: (e: TargetedEvent) => void 97 | } 98 | 99 | export type IAzureMapPopupEvent = { 100 | eventName: keyof PopupEvents 101 | callback: (e: TargetedEvent) => void 102 | } 103 | 104 | export type IAzureMapMouseEvents = { 105 | [T in keyof HtmlMarkerEvents]: (e: TargetedEvent) => void 106 | } 107 | 108 | export type IAzureMapHtmlMarker = { 109 | id?: string 110 | isPopupVisible?: boolean 111 | markerContent?: ReactElement 112 | options?: HtmlMarkerOptions 113 | events?: Array 114 | } 115 | 116 | export type IAzureMapPopup = { 117 | isVisible?: boolean 118 | options?: PopupOptions 119 | events?: Array 120 | popupContent: ReactElement 121 | } 122 | 123 | export type IAzureMapDataSourceContextState = { 124 | dataSourceRef: atlas.source.DataSource | atlas.source.VectorTileSource | null 125 | } 126 | 127 | export type IAzureMapLayerContextState = { 128 | layerRef: atlas.layer.SymbolLayer | atlas.layer.ImageLayer | atlas.layer.TileLayer | null 129 | } 130 | 131 | export type IAzureDataSourceChildren = 132 | | (IAzureMapFeature & ReactNode) 133 | | ReactElement 134 | | ReactElement 135 | 136 | export type IAzureVectorTileSourceChildren = ReactElement 137 | 138 | export type IAzureMapDataSourceEvent = { 139 | [property in IAzureMapDataSourceEventType]: (e: Shape[]) => void 140 | } 141 | 142 | export type IAzureMapVectorTileSourceEvent = { 143 | [property in IAzureMapSourceEventType]?: (e: atlas.source.VectorTileSource) => void 144 | } 145 | 146 | export type IAzureMapEvent = { 147 | [property in IAzureMapEventsType]: ( 148 | e: 149 | | MapDataEvent 150 | | MapErrorEvent 151 | | MapTouchEvent 152 | | MapMouseEvent 153 | | string 154 | | MapMouseWheelEvent 155 | | MapEvent 156 | | atlas.layer.Layer 157 | | atlas.source.Source 158 | ) => void 159 | } 160 | 161 | export type IAzureDataSourceStatefulProviderProps = { 162 | id: string 163 | children?: 164 | | Array 165 | | IAzureDataSourceChildren 166 | | null 167 | options?: DataSourceOptions 168 | events?: IAzureMapDataSourceEvent | any 169 | dataFromUrl?: string 170 | collection?: 171 | | atlas.data.FeatureCollection 172 | | atlas.data.Feature 173 | | atlas.data.Geometry 174 | | atlas.data.GeometryCollection 175 | | Shape 176 | | Array | atlas.data.Geometry | Shape> 177 | index?: number 178 | } 179 | 180 | export type IAzureVectorTileSourceStatefulProviderProps = { 181 | id: string 182 | children?: 183 | | Array 184 | | IAzureVectorTileSourceChildren 185 | | null 186 | options?: VectorTileSourceOptions 187 | events?: IAzureMapVectorTileSourceEvent 188 | // NOTE: not sure yet why this is needed, haven't seen this used in AzureMapsDataSource, though IAzureGeoJSONDataSourceStatefulProviderProps has it 189 | index?: number 190 | } 191 | 192 | export type IAzureMapLayerEvent = { 193 | [property in IAzureMapLayerEventType]: ( 194 | e: MapMouseEvent | MapTouchEvent | MapMouseWheelEvent 195 | ) => void 196 | } 197 | 198 | export type IAzureMapLifecycleEvent = { 199 | [property in IAzureMapLayerLifecycleEvents]: (e: atlas.layer.Layer) => void 200 | } 201 | 202 | export interface IAzureLayerStatefulProviderOptions 203 | extends SymbolLayerOptions, 204 | HeatMapLayerOptions, 205 | ImageLayerOptions, 206 | LineLayerOptions, 207 | PolygonExtrusionLayerOptions, 208 | PolygonLayerOptions, 209 | TileLayerOptions, 210 | BubbleLayerOptions, 211 | LayerOptions { 212 | opacity: HeatMapLayerOptions['opacity'] & 213 | ImageLayerOptions['opacity'] & 214 | TileLayerOptions['opacity'] 215 | color: HeatMapLayerOptions['color'] & BubbleLayerOptions['color'] 216 | radius: HeatMapLayerOptions['radius'] & BubbleLayerOptions['radius'] 217 | fillOpacity: PolygonExtrusionLayerOptions['fillOpacity'] & PolygonLayerOptions['fillOpacity'] 218 | } 219 | 220 | export type IAzureLayerStatefulProviderProps = { 221 | id?: string 222 | options?: IAzureLayerStatefulProviderOptions | Options 223 | type: IAzureMapLayerType 224 | events?: IAzureMapLayerEvent | any 225 | onCreateCustomLayer?: (dataSourceRef: DataSourceType, mapRef: MapType | null) => atlas.layer.Layer 226 | lifecycleEvents?: IAzureMapLifecycleEvent | any 227 | } 228 | 229 | export type IAzureMapLayerLifecycleEvents = 'layeradded' | 'layerremoved' 230 | 231 | export type IAzureMapEventsType = 232 | | IAzureMapLayerEventType 233 | | IAzureMapLayerLifecycleEvents 234 | | IAzureMapDataSourceEventType 235 | | IAzureMapAddEventsType 236 | | IAzureMapSourceEventType 237 | // Adds a data event to the map. 238 | | 'data' 239 | | 'sourcedata' 240 | | 'styledata' 241 | // Adds an event to the map. 242 | | 'error' 243 | // Adds a style image missing event to the map. 244 | | 'styleimagemissing' 245 | 246 | export type IAzureMapAddEventsType = 247 | | 'boxzoomstart' 248 | | 'boxzoomend' 249 | | 'dragstart' 250 | | 'drag' 251 | | 'dragend' 252 | | 'idle' 253 | | 'load' 254 | | 'movestart' 255 | | 'move' 256 | | 'moveend' 257 | | 'pitchstart' 258 | | 'pitch' 259 | | 'pitchend' 260 | | 'ready' 261 | | 'render' 262 | | 'resize' 263 | | 'rotatestart' 264 | | 'rotate' 265 | | 'rotateend' 266 | | 'tokenacquired' 267 | | 'zoomstart' 268 | | 'zoom' 269 | | 'zoomend' 270 | 271 | export type IAzureMapDataSourceEventType = 'dataadded' | 'dataremoved' 272 | 273 | export type IAzureMapSourceEventType = 'sourceadded' | 'sourceremoved' 274 | 275 | export type IAzureMapLayerEventType = 276 | // Mouse events 277 | | 'mousedown' 278 | | 'mouseup' 279 | | 'mouseover' 280 | | 'mousemove' 281 | | 'click' 282 | | 'dblclick' 283 | | 'mouseout' 284 | | 'mouseenter' 285 | | 'mouseleave' 286 | | 'contextmenu' 287 | // Wheel events 288 | | 'wheel' 289 | // Touch events 290 | | 'touchstart' 291 | | 'touchend' 292 | | 'touchmove' 293 | | 'touchcancel' 294 | 295 | export type IAzureMapLayerType = 296 | | 'SymbolLayer' 297 | | 'HeatLayer' 298 | | 'ImageLayer' 299 | | 'LineLayer' 300 | | 'PolygonExtrusionLayer' 301 | | 'PolygonLayer' 302 | | 'TileLayer' 303 | | 'BubbleLayer' 304 | | 'HtmlMarkerLayer' 305 | | 'custom' 306 | 307 | export type IAzureMapFeatureType = 308 | | 'Point' 309 | | 'MultiPoint' 310 | | 'LineString' 311 | | 'MultiLineString' 312 | | 'Polygon' 313 | | 'MultiPolygon' 314 | 315 | export type IAzureMapFeature = { 316 | id?: string 317 | type: IAzureMapFeatureType 318 | coordinate?: atlas.data.Position 319 | coordinates?: Array 320 | multipleCoordinates?: Array> 321 | multipleDimensionCoordinates?: Array>> 322 | bbox?: atlas.data.BoundingBox 323 | variant?: IAzureMapFeatureVariant 324 | properties?: Options 325 | // Shape functions: 326 | setCoords?: 327 | | atlas.data.Position 328 | | atlas.data.Position[] 329 | | atlas.data.Position[][] 330 | | atlas.data.Position[][][] 331 | setProperties?: Options 332 | } 333 | 334 | export type IAzureMapLayerProps = IAzureMapLayerContextState 335 | export type IAzureMapMouseEventRef = HtmlMarker // && other possible iterfaces 336 | export type IAzureMapsContextProps = IAzureMapContextState 337 | export type IAzureMapDataSourceProps = IAzureMapDataSourceContextState 338 | export type DataSourceType = atlas.source.DataSource | atlas.source.VectorTileSource 339 | export type LayerType = atlas.layer.SymbolLayer | atlas.layer.ImageLayer | atlas.layer.TileLayer 340 | export type MapType = atlas.Map 341 | export type GeometryType = atlas.data.Geometry 342 | export type FeatureType = atlas.data.Feature 343 | export type ShapeType = atlas.Shape 344 | export type IAzureMapFeatureVariant = 'shape' | 'feature' 345 | 346 | // Azure types 347 | export type AzureDataLineString = atlas.data.LineString 348 | export type AzureDataPosition = atlas.data.Position 349 | export type ControlOptions = atlas.ControlOptions 350 | export type AzureSetCameraOptions = ((CameraOptions | CameraBoundsOptions) & AnimationOptions) | any 351 | export { AuthenticationType } from 'azure-maps-control' 352 | -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { fork } = require("child_process") 3 | const colors = require("colors") 4 | 5 | const { readFileSync, writeFileSync } = require("fs") 6 | const pkg = JSON.parse( 7 | readFileSync(path.resolve(__dirname, "..", "package.json")) 8 | ) 9 | 10 | pkg.scripts.prepush = "npm run test:prod && npm run build" 11 | pkg.scripts.commitmsg = "commitlint -E HUSKY_GIT_PARAMS" 12 | 13 | writeFileSync( 14 | path.resolve(__dirname, "..", "package.json"), 15 | JSON.stringify(pkg, null, 2) 16 | ) 17 | 18 | // Call husky to set up the hooks 19 | fork(path.resolve(__dirname, "..", "node_modules", "husky", "lib", "installer", 'bin'), ['install']) 20 | 21 | console.log() 22 | console.log(colors.green("Done!!")) 23 | console.log() 24 | 25 | if (pkg.repository.url.trim()) { 26 | console.log(colors.cyan("Now run:")) 27 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 28 | console.log(colors.cyan(" semantic-release-cli setup")) 29 | console.log() 30 | console.log( 31 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 32 | ) 33 | console.log() 34 | console.log( 35 | colors.gray( 36 | 'Note: Make sure "repository.url" in your package.json is correct before' 37 | ) 38 | ) 39 | } else { 40 | console.log( 41 | colors.red( 42 | 'First you need to set the "repository.url" property in package.json' 43 | ) 44 | ) 45 | console.log(colors.cyan("Then run:")) 46 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 47 | console.log(colors.cyan(" semantic-release-cli setup")) 48 | console.log() 49 | console.log( 50 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 51 | ) 52 | } 53 | 54 | console.log() 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "skipLibCheck": true, 5 | "moduleResolution": "node", 6 | "target": "es5", 7 | "module": "es2015", 8 | "lib": ["es2015", "es2016", "es2017", "dom"], 9 | "strict": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "allowSyntheticDefaultImports": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "declarationDir": "dist/types", 16 | "outDir": "dist/lib", 17 | "typeRoots": ["node_modules/@types"], 18 | "jsx": "react", 19 | "types": ["jest"] 20 | }, 21 | "exclude": ["node_modules"], 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } --------------------------------------------------------------------------------