├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── stale.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── OpenStreetMap.json ├── README.md ├── debug ├── README.md └── edge-of-the-world.html ├── example.png ├── examples ├── basemap-places.html ├── contour-dev.html ├── custom-vtl-dev.html ├── customize-basemap-style.html ├── customize-vtl-style.html ├── gallery-dev.html ├── languages.html ├── open-basemaps.html ├── osm-basemaps.html ├── quickstart-dev.html ├── quickstart-prod.html ├── rtl-language.html └── worldview.html ├── index.d.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── profiles ├── base.js ├── debug.js └── production.js ├── scripts └── release.sh ├── spec ├── UtilSpec.js ├── VectorBasemapLayerSpec.js └── VectorTileLayerSpec.js └── src ├── EsriLeafletVector.js ├── MaplibreGLLayer.js ├── Util.js ├── VectorBasemapLayer.js └── VectorTileLayer.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-semistandard', 3 | plugins: ['chai-friendly'], 4 | rules: { 5 | 'no-var': 'off', 6 | 'no-unused-expressions': 0, 7 | 'chai-friendly/no-unused-expressions': 2 8 | }, 9 | globals: { 10 | L: false 11 | }, 12 | ignorePatterns: ['spec/**/*.js', 'eslintrc.js'] 13 | }; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: Report an issue with Esri Leaflet Vector 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to fill out this bug report! This is for reporting bugs with Esri Leaflet Vector specifically. If you have an issue with Esri Leaflet itself, please go to https://github.com/Esri/esri-leaflet 8 | - type: textarea 9 | id: bug-description 10 | attributes: 11 | label: Describe the bug 12 | description: Please include a clear and concise description of the bug. If you intend to submit a PR for this issue, tell us in the description. Thanks! 13 | placeholder: Bug description 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: reproduction 18 | attributes: 19 | label: Reproduction 20 | description: A link to a public repository or jsbin (you can start with https://jsbin.com/jexevoy/edit) that reproduces the issue, along with a step by step explanation of how to see the issue. If no reproduction case is provided within a reasonable time-frame, the issue will be closed. 21 | placeholder: Reproduction 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: logs 26 | attributes: 27 | label: Logs 28 | description: "Please include browser console around the time this bug occurred. Please try not to insert an image but copy paste the log text." 29 | render: shell 30 | - type: textarea 31 | id: system-info 32 | attributes: 33 | label: System Info 34 | description: Please include which version of Leaflet you are using, which version of Esri Leaflet you are using, and output of `npx envinfo --system --binaries --browsers --npmPackages "{leaflet,esri-leaflet}"` 35 | render: shell 36 | placeholder: "Leaflet version: `v_._._`. Esri Leaflet version: `v_._._`. Esri Leaflet Vector version: `v_._._`. ..." 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: additional-context 41 | attributes: 42 | label: Additional Information 43 | description: Add any other context about the problem here. If you're are *not* using the CDN, please note what loading/bundling library you are using (webpack, rollup, vite, etc)? 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentation 4 | url: https://github.com/Esri/esri-leaflet/issues/new/choose 5 | about: To suggest an idea or report an issue with the documentation site, https://developers.arcgis.com/esri-leaflet, please log the issue in the Esri Leaflet repository. 6 | - name: Esri Community 7 | url: https://community.esri.com/t5/esri-leaflet/ct-p/esri-leaflet 8 | about: Ask questions and discuss with other Esri Leaflet users. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | description: Suggest an idea for Esri Leaflet Vector 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to request this feature! 8 | - type: textarea 9 | id: problem 10 | attributes: 11 | label: Describe the problem 12 | description: Please provide a clear and concise description the problem this feature would solve. The more information you can provide here, the better. 13 | placeholder: I'm always frustrated when... 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: solution 18 | attributes: 19 | label: Describe the proposed solution 20 | description: Please provide a clear and concise description of what you would like to happen. 21 | placeholder: I would like to see... 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: Alternatives considered 28 | description: "Please provide a clear and concise description of any alternative solutions or features you've considered." 29 | - type: textarea 30 | id: additional-context 31 | attributes: 32 | label: Additional Information 33 | description: Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 30 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: ["Comments Needed"] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | - security 17 | 18 | # Set to true to ignore issues in a project (defaults to false) 19 | exemptProjects: false 20 | 21 | # Set to true to ignore issues in a milestone (defaults to false) 22 | exemptMilestones: false 23 | 24 | # Set to true to ignore issues with an assignee (defaults to false) 25 | exemptAssignees: false 26 | 27 | # Label to use when marking as stale 28 | staleLabel: stale 29 | 30 | # Comment to post when marking as stale. Set to `false` to disable 31 | markComment: > 32 | This issue has been automatically marked as stale because we're waiting on 33 | more information or details, but have not received any response. It will be 34 | closed if no further activity occurs. Thank you! 35 | # Comment to post when removing the stale label. 36 | # unmarkComment: > 37 | # Your comment here. 38 | 39 | # Comment to post when closing a stale Issue or Pull Request. 40 | # closeComment: > 41 | # This issue was 42 | 43 | # Limit the number of actions per hour, from 1-30. Default is 30 44 | limitPerRun: 30 45 | # Limit to only `issues` or `pulls` 46 | # only: issues 47 | 48 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 49 | # pulls: 50 | # daysUntilStale: 30 51 | # markComment: > 52 | # This pull request has been automatically marked as stale because it has not had 53 | # recent activity. It will be closed if no further activity occurs. Thank you 54 | # for your contributions. 55 | 56 | # issues: 57 | # exemptLabels: 58 | # - confirmed -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | env: 4 | NODE_VERSION: 16 5 | jobs: 6 | setup: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest] 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ env.NODE_VERSION }} 19 | check-latest: true 20 | cache: npm 21 | 22 | - name: Cache dependencies 23 | id: cache-dependencies 24 | uses: actions/cache@v3 25 | with: 26 | path: node_modules 27 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 28 | 29 | - name: Install dependencies 30 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 31 | run: npm ci 32 | 33 | - name: Cache setup 34 | uses: actions/cache@v3 35 | with: 36 | path: ./* 37 | key: ${{ runner.os }}-${{ github.sha }}-setup 38 | 39 | build: 40 | needs: setup 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | matrix: 44 | os: [ubuntu-latest, windows-latest] 45 | steps: 46 | - name: Restore setup 47 | uses: actions/cache@v3 48 | with: 49 | path: ./* 50 | key: ${{ runner.os }}-${{ github.sha }}-setup 51 | 52 | - name: Set up Node 53 | uses: actions/setup-node@v3 54 | with: 55 | node-version: ${{ env.NODE_VERSION }} 56 | 57 | - name: Build project 58 | run: npm run build 59 | 60 | - name: Cache build 61 | uses: actions/cache@v3 62 | with: 63 | path: ./* 64 | key: ${{ runner.os }}-${{ github.sha }}-build 65 | 66 | lint: 67 | needs: setup 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Restore setup 71 | uses: actions/cache@v3 72 | with: 73 | path: ./* 74 | key: ${{ runner.os }}-${{ github.sha }}-setup 75 | 76 | - name: Set up Node 77 | uses: actions/setup-node@v3 78 | with: 79 | node-version: ${{ env.NODE_VERSION }} 80 | 81 | - name: Run lint task 82 | run: npm run lint 83 | 84 | test: 85 | needs: build 86 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 87 | strategy: 88 | fail-fast: false 89 | matrix: 90 | include: 91 | - browser: Chrome1280x1024 92 | - browser: FirefoxTouch 93 | - browser: FirefoxNoTouch 94 | - browser: Edge 95 | os: windows-latest 96 | steps: 97 | - name: Restore build 98 | uses: actions/cache@v3 99 | with: 100 | path: ./* 101 | key: ${{ runner.os }}-${{ github.sha }}-build 102 | 103 | - name: Set up Node 104 | uses: actions/setup-node@v3 105 | with: 106 | node-version: ${{ env.NODE_VERSION }} 107 | 108 | - name: Run tests on ${{ matrix.browser }} 109 | run: npm test -- --browsers ${{ matrix.browser }} 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | dist/ 36 | debug/*.html 37 | !debug/edge-of-the-world.html 38 | !debug/sample.html 39 | 40 | *.code-workspace 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Upcoming changes][unreleased] 7 | 8 | ## [4.3.0] - 2025-06-03 9 | 10 | - All `osm/*` basemap styles are now deprecated and will produce a warning when used. 11 | - Support for the new `open/*` styles added to replace `osm/*`. 12 | - Assets for `VectorTileLayer` items will now use the CDN to improve performance if possable. 13 | 14 | ## [4.2.7] - 2025-01-02 15 | 16 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `4.7.1` ([#230](https://github.com/Esri/esri-leaflet-vector/pull/230)). 17 | 18 | ### Updated 19 | 20 | - Better console messages when API key is invalid ([#227](https://github.com/Esri/esri-leaflet-vector/pull/227)) 21 | 22 | ## [4.2.6] - 2024-12-17 23 | 24 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `4.6.0`. 25 | 26 | ### Fixed 27 | 28 | - Fixed issue where Esri Attribution was not added properly when using vectorBasemapLayer using an item ID ([#229](https://github.com/Esri/esri-leaflet-vector/pull/229)) 29 | 30 | ## [4.2.5] - 2024-08-26 31 | 32 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `4.6.0`. 33 | 34 | ## [4.2.4] - 2024-08-21 35 | 36 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `4.5.0`. 37 | 38 | ### Fixed 39 | 40 | - Fixed issue where Esri Attribution was not removed when VectorBasemapLayer was removed ([#208](https://github.com/Esri/esri-leaflet-vector/pull/208)) 41 | 42 | ### Updated 43 | 44 | - Updated maplibre-gl dependency to v4 ([#219](https://github.com/Esri/esri-leaflet-vector/pull/219)) 45 | - Updated dependencies ([#223](https://github.com/Esri/esri-leaflet-vector/pull/223)) 46 | 47 | ## [4.2.3] - 2023-12-07 48 | 49 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `3.3.1`. 50 | 51 | ### Added 52 | 53 | - Added worldview and places params ([#214](https://github.com/Esri/esri-leaflet-vector/pull/214)) 54 | 55 | ### Fixed 56 | 57 | - Fixed `wrong listener type: undefined` console warning ([#211](https://github.com/Esri/esri-leaflet-vector/pull/211)) 58 | 59 | ## [4.2.2] - 2023-10-23 60 | 61 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `3.3.1`. 62 | 63 | ### Fixed 64 | 65 | - Adds support for RTL language labels by exposing the maplibre `setRTLTextPlugin` method ([#207](https://github.com/Esri/esri-leaflet-vector/pull/207)) 66 | 67 | ## [4.2.1] - 2023-10-18 68 | 69 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `3.3.1`. 70 | 71 | ### Updated 72 | 73 | - Updated maplibre-gl dependency to v3 ([#201](https://github.com/Esri/esri-leaflet-vector/pull/201)) 74 | 75 | ## [4.2.0] - 2023-10-17 76 | 77 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `2.3.0`. 78 | 79 | ### Added 80 | 81 | - Allow users to set preserveDrawingBuffer in the options ([#199](https://github.com/Esri/esri-leaflet-vector/pull/199)) 82 | - Expose MaplibreGLJSLayer variable ([#197](https://github.com/Esri/esri-leaflet-vector/pull/197)) 83 | 84 | ### Fixed 85 | 86 | - Token with sprite and glyphs ([#192](https://github.com/Esri/esri-leaflet-vector/pull/192)) 87 | - Export of package version ([#187](https://github.com/Esri/esri-leaflet-vector/pull/187)) 88 | 89 | ## [4.1.0] - 2023-05-31 90 | 91 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `2.3.0`. 92 | 93 | ### Added 94 | 95 | - Added `load-error` event ([#165](https://github.com/Esri/esri-leaflet-vector/pull/165)) 96 | - Added support for the v2 basemap styles service ([#182](https://github.com/Esri/esri-leaflet-vector/pull/182)) 97 | 98 | ### Fixed 99 | 100 | - fix default pane for VectorBasemapLayer ([#182](https://github.com/Esri/esri-leaflet-vector/pull/182)) 101 | 102 | ### Changed 103 | 104 | - VectorBasemapLayer now inherits from VectorTileLayer, reducing code duplication within this repo. ([#182](https://github.com/Esri/esri-leaflet-vector/pull/182)) 105 | 106 | ## [4.0.2] - 2023-04-04 107 | 108 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `2.3.0`. 109 | 110 | ### Fixed 111 | 112 | - Fixed layer invisible at certain zoom levels using minLOD/maxLOD ([#166](https://github.com/Esri/esri-leaflet-vector/pull/166)) 113 | - Added `index.d.ts` to release ([#167](https://github.com/Esri/esri-leaflet-vector/pull/167)) 114 | 115 | ## [4.0.1] - 2023-02-23 116 | 117 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `2.3.0`. 118 | 119 | ### Fixed 120 | 121 | - Switching basemaps issue ([#162](https://github.com/Esri/esri-leaflet-vector/pull/162)) 122 | 123 | ### Updated 124 | 125 | - Updated dependencies ([#163](https://github.com/Esri/esri-leaflet-vector/pull/163)) 126 | 127 | ## [4.0.0] - 2022-09-02 128 | 129 | MapLibre GL JS version that is included with this version of Esri Leaflet Vector: `2.3.0`. 130 | 131 | ### Updated 132 | 133 | - Switched to use Maplibre GL JS ([#141](https://github.com/Esri/esri-leaflet-vector/pull/141)) 134 | 135 | ## [3.1.5] - 2022-08-23 136 | 137 | ### Fixed 138 | 139 | - Fixed issue where layer is mis-aligned with map when panning the map off screen ([#144](https://github.com/Esri/esri-leaflet-vector/pull/144)) 140 | 141 | ### Updated 142 | 143 | - Updated dependencies 144 | 145 | ## [3.1.4] - 2022-08-12 146 | 147 | ### Fixed 148 | 149 | - Added "Powered by Esri" when adding a layer via item ID for consistency ([#135](https://github.com/Esri/esri-leaflet-vector/pull/135)) 150 | 151 | ## [3.1.3] - 2022-05-23 152 | 153 | ### Fixed 154 | 155 | - Offset issue when zooming out to levels 0 or 1 ([#127](https://github.com/Esri/esri-leaflet-vector/pull/127)) 156 | 157 | ### Updated 158 | 159 | - Updated dependencies 160 | 161 | ## [3.1.2] - 2022-03-03 162 | 163 | ### Added 164 | 165 | - TypeScript types file ([#114](https://github.com/Esri/esri-leaflet-vector/pull/114)) 166 | 167 | ### Updated 168 | 169 | - Updated dependencies and changed build-related settings to be consistent with Esri Leaflet ([#122](https://github.com/Esri/esri-leaflet-vector/pull/122)) 170 | 171 | ## [3.1.1] - 2021-11-09 172 | 173 | ### Fixed 174 | 175 | - Map panning was broken in some environments due to a specific `mapbox-gl-js` version. Pinning this library's `package.json` specifically to `mapbox-gl-js v1.13.1` fixes the issue. [#105](https://github.com/Esri/esri-leaflet-vector/pull/105) 176 | 177 | ## [3.1.0] - 2021-08-09 178 | 179 | ### Added 180 | 181 | - `L.esri.Vector.vectorTileLayer` has been extended to support vector tiles layers hosted in ArcGIS Enterprise. A new `portalUrl` layer constructor option was added and is intended to be used with the "ITEM_ID" constructor flavor. [#97](https://github.com/Esri/esri-leaflet-vector/pull/97) 182 | 183 | - New README documentation and a developer console warning for `L.esri.Vector.vectorTileLayer` explaining that only services with a Web Mercator `spatialReference` are fully supported. [#95](https://github.com/Esri/esri-leaflet-vector/pull/95) 184 | 185 | - Updated peerDependencies to be more flexible for using v3 of `esri-leaflet`. [#99](https://github.com/Esri/esri-leaflet-vector/pull/99) 186 | 187 | ### Fixed 188 | 189 | - Utility functions used by `L.esri.Vector.vectorTileLayer` have been improved to be more friendly with URL structures and style reformatting assumptions. [#100](https://github.com/Esri/esri-leaflet-vector/pull/100) 190 | 191 | ## [3.0.1] - 2021-06-03 192 | 193 | ### Fixed 194 | 195 | - While formatting the style object when loading a new `L.esri.Vector.vectorTileLayer`, check first if layer layout property exists before accessing. (🙏jag-eagle-technology🙏 [#70](https://github.com/Esri/esri-leaflet-vector/pull/70)) 196 | 197 | - Support style items with non-esri source names. [#91](https://github.com/Esri/esri-leaflet-vector/pull/91) 198 | 199 | ### Changed 200 | 201 | - The layer constructor option `apikey` in all lowercase is now supported **and encouraged** in order to be consistent with the rest of esri-leaflet's ecosystem. Note that camel case `apiKey` continues to be allowed since [3.0.0]. [#89](https://github.com/Esri/esri-leaflet-vector/pull/89) 202 | 203 | ## [3.0.0] - 2021-01-25 204 | 205 | ### Breaking 206 | 207 | - `L.esri.Vector.basemap` is now `L.esri.Vector.vectorBasemapLayer` and requires an API key (`apiKey`) or token (`token`). 208 | - `L.esri.Vector.layer` is now `L.esri.Vector.vectorTileLayer`. 209 | - Simplified imports. `mapbox-gl-js v1` continues to be a depedency but is bundled internally with production builds. 210 | 211 | ```html 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | ``` 220 | 221 | ## [2.0.2] 222 | 223 | ### Added 224 | 225 | - New basemaps! 🙏pmacMaps🙏 226 | - `DarkHumanGeography` 227 | - `DarkHumanGeographyDetail` 228 | - `ChartedTerritory` 229 | - `MidCentury` 230 | - `Nova` 231 | 232 | ## [2.0.1] 233 | 234 | ### Fixed 235 | 236 | - Added support for deeper zoom by artificially capping tile requests at zoom level 15. 237 | 238 | ## [2.0.0] 239 | 240 | ### Changed 241 | 242 | - Existing basemaps have been updated to [`v2`](https://www.esri.com/arcgis-blog/products/arcgis-living-atlas/mapping/whats-new-in-esri-vector-basemaps-december-2017/) 243 | 244 | ### Added 245 | 246 | - Esri's [OpenStreetMap Vector basemap](https://www.esri.com/arcgis-blog/products/arcgis-living-atlas/mapping/new-osm-vector-basemap/) 247 | 248 | ### Breaking Change 249 | 250 | - `mapbox-gl-js` is now an external dependency. it is no longer bundled internally. 251 | 252 | ```html 253 | 254 | 255 | 256 | 257 | 258 | 259 | ``` 260 | 261 | ## [1.0.7] 262 | 263 | ### Fixed 264 | 265 | - several edge cases that corrupted the current state of the map 266 | 267 | ## [1.0.6] 268 | 269 | ### Changed 270 | 271 | - now using Esri's latest and greatest basemaps 272 | 273 | ### Fixed 274 | 275 | - Ensure that when a tileMap is present in an ArcGIS Pro published tileset, that its url is concatenated correctly [#20](https://github.com/Esri/esri-leaflet-vector/issues/20) 276 | 277 | ## [1.0.5] 278 | 279 | ### Fixed 280 | 281 | - Fixed a regression which caused `L.esri.Vector.Layer` not to honor custom styles applied to generic Esri hosted vector tilesets (example [item](http://www.arcgis.com/home/item.html?id=bd505ce3efff479bb4e87b182f180159)) 282 | 283 | ## [1.0.4] 284 | 285 | ### Added 286 | 287 | - `L.esri.Vector.layer` can now be used to display Vector Tile Services published using ArcGIS Pro. (like [this one](http://www.arcgis.com/home/item.html?id=0bac0ffdc8634d9a9bc662bb8fa7547d)) 288 | 289 | ## [1.0.3] 290 | 291 | ### Added 292 | 293 | - `L.esri.Vector.layer` object added so that developers can point at any arbitrary ArcGIS Online hosted vector tile source 294 | 295 | ### Fixed 296 | 297 | - trapped situation in which vector style json defines path of sprites/glyphs using fully qualified paths. 298 | 299 | ### Changed 300 | 301 | - made dependency on Leaflet fixed at `1.0.0-beta.2` (until [#47](https://github.com/mapbox/mapbox-gl-leaflet/issues/47) is resolved) 302 | - started linting all the `.js` in the repo 303 | 304 | ## [1.0.2] 305 | 306 | ### Fixed 307 | 308 | - Added three new Vector basemaps. [Mid-Century](http://www.arcgis.com/home/item.html?id=763884983d3544c0a418a97992881fce), [Newspaper](http://www.arcgis.com/home/item.html?id=4f4843d99c34436f82920932317893ae) and [Spring](http://www.arcgis.com/home/item.html?id=267f44f08a844c7abee2b62b00600540). 309 | 310 | ## [1.0.1] 311 | 312 | ### Fixed 313 | 314 | - added .npmignore file to ensure built library is included in npm package. 315 | 316 | ## 1.0.0 317 | 318 | ### Added 319 | 320 | - Initial Release 321 | 322 | [unreleased]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.7...HEAD 323 | [4.2.7]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.6...v4.2.7 324 | [4.2.6]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.5...v4.2.6 325 | [4.2.5]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.4...v4.2.5 326 | [4.2.4]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.3...v4.2.4 327 | [4.2.3]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.2...v4.2.3 328 | [4.2.2]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.1...v4.2.2 329 | [4.2.1]: https://github.com/esri/esri-leaflet-vector/compare/v4.2.0...v4.2.1 330 | [4.2.0]: https://github.com/esri/esri-leaflet-vector/compare/v4.1.0...v4.2.0 331 | [4.1.0]: https://github.com/esri/esri-leaflet-vector/compare/v4.0.2...v4.1.0 332 | [4.0.2]: https://github.com/esri/esri-leaflet-vector/compare/v4.0.1...v4.0.2 333 | [4.0.1]: https://github.com/esri/esri-leaflet-vector/compare/v4.0.0...v4.0.1 334 | [4.0.0]: https://github.com/esri/esri-leaflet-vector/compare/v3.1.5...v4.0.0 335 | [3.1.4]: https://github.com/esri/esri-leaflet-vector/compare/v3.1.3...v3.1.4 336 | [3.1.3]: https://github.com/esri/esri-leaflet-vector/compare/v3.1.2...v3.1.3 337 | [3.1.2]: https://github.com/esri/esri-leaflet-vector/compare/v3.1.0...v3.1.2 338 | [3.1.1]: https://github.com/esri/esri-leaflet-vector/compare/v3.1.0...v3.1.1 339 | [3.1.0]: https://github.com/esri/esri-leaflet-vector/compare/v3.0.1...v3.1.0 340 | [3.0.1]: https://github.com/esri/esri-leaflet-vector/compare/v3.0.0...v3.0.1 341 | [3.0.0]: https://github.com/esri/esri-leaflet-vector/compare/v2.0.2...v3.0.0 342 | [2.0.2]: https://github.com/esri/esri-leaflet-vector/compare/v2.0.1...v2.0.2 343 | [2.0.1]: https://github.com/esri/esri-leaflet-vector/compare/v2.0.0...v2.0.1 344 | [2.0.0]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.7...v2.0.0 345 | [1.0.7]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.6...v1.0.7 346 | [1.0.6]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.5...v1.0.6 347 | [1.0.5]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.4...v1.0.5 348 | [1.0.4]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.3...v1.0.4 349 | [1.0.3]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.2...v1.0.3 350 | [1.0.2]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.1...v1.0.2 351 | [1.0.1]: https://github.com/esri/esri-leaflet-vector/compare/v1.0.0...v1.0.1 352 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright (c) 2014-2024 Environmental Systems Research Institute, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Esri Leaflet Vector Tile Plugin 2 | 3 | [![npm version][npm-img]][npm-url] 4 | [![apache licensed](https://img.shields.io/badge/license-Apache-green.svg?style=flat-square)](https://raw.githubusercontent.com/Esri/esri-leaflet-vector/master/LICENSE) 5 | 6 | [npm-img]: https://img.shields.io/npm/v/esri-leaflet-vector.svg?style=flat-square 7 | [npm-url]: https://www.npmjs.com/package/esri-leaflet-vector 8 | 9 | > A plugin for Esri Leaflet to visualize Vector tiles from ArcGIS Online. 10 | 11 | ## Example 12 | 13 | Take a look at the [live demo](https://developers.arcgis.com/esri-leaflet/samples/showing-a-basemap/). 14 | 15 | ![Example Image](example.png) 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 | Esri Leaflet Vector Basemap 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | 50 |
51 | 52 | 59 | 60 | 61 | 62 | ``` 63 | 64 | ## API Reference 65 | 66 | ### [`L.esri.Vector.vectorBasemapLayer`](https://developers.arcgis.com/esri-leaflet/api-reference/layers/vector-basemap/) 67 | 68 | For rendering basemap layers which use the Esri Basemap Styles API internally. Extends [L.Layer](https://leafletjs.com/reference#layer). 69 | 70 | 71 | ```javascript 72 | // example using an Esri Basemap Styles API name 73 | L.esri.Vector.vectorBasemapLayer("ArcGIS:Streets", { 74 | // provide either `apikey` or `token` 75 | apikey: "...", 76 | token: "..." 77 | }).addTo(map); 78 | ``` 79 | 80 | ```javascript 81 | // example using an ITEM_ID 82 | L.esri.Vector.vectorBasemapLayer("ITEM_ID", { 83 | // provide either `apikey` or `token` 84 | apikey: "...", 85 | token: "..." 86 | }).addTo(map); 87 | ``` 88 | 89 | #### Basemap Names 90 | 91 | Please see [the documentation](https://developers.arcgis.com/esri-leaflet/api-reference/layers/vector-basemap/#vector-basemaps) for a list of basemap names you can use (`ArcGIS:Streets`, `ArcGIS:DarkGray`, `ArcGIS:Imagery:Standard`, `OSM:Standard`, etc). 92 | 93 | ### [`L.esri.Vector.vectorTileLayer`](https://developers.arcgis.com/esri-leaflet/api-reference/layers/vector-layer/) 94 | 95 | For custom vector tiles layers published from user data. Extends [L.Layer](https://leafletjs.com/reference#layer). 96 | 97 | :warning: This only supports services using the Web Mercator projection because it [relies directly upon `maplibre-gl-js`](#dependencies). Otherwise, the layer is not guaranteed to display properly. More information is available at [Maplibre custom coordinate system](https://maplibre.org/roadmap/non-mercator-projection/). 98 | 99 | ```javascript 100 | // example using an ITEM_ID 101 | L.esri.Vector.vectorTileLayer("ITEM_ID", { 102 | // optional: provide either `apikey` or `token` if not public 103 | apikey: "...", 104 | token: "...", 105 | 106 | // optional: if your layer is not hosted on ArcGIS Online, 107 | // change `portalUrl` to the ArcGIS Enterprise base url 108 | // (this is necessary when specifying an ITEM_ID) 109 | portalUrl: "https://www.arcgis.com", // default value 110 | 111 | // optional: customize the style with a function that gets the default style from the service 112 | // and returns the new style to be used 113 | style: (style) => { 114 | return newStyle; 115 | } 116 | }).addTo(map); 117 | ``` 118 | 119 | ```javascript 120 | // example using a VectorTileServer SERVICE_URL 121 | L.esri.Vector.vectorTileLayer("SERVICE_URL", { 122 | // optional: provide either `apikey` or `token` if not public 123 | apikey: "...", 124 | token: "...", 125 | 126 | // optional: if your layer is not hosted on ArcGIS Online, 127 | // change `portalUrl` to the ArcGIS Enterprise base url 128 | // (this may not be necessary when specifying a SERVICE_URL) 129 | portalUrl: "https://www.arcgis.com", // default value 130 | 131 | // optional: set by default to `false` for performance reasons 132 | // set to `true` to resolve WebGL printing issues in Firefox 133 | preserveDrawingBuffer: false, // default value 134 | 135 | // optional: customize the style with a function that gets the default style from the service 136 | // and returns the new style to be used 137 | style: (style) => { 138 | return newStyle; 139 | } 140 | }).addTo(map); 141 | ``` 142 | 143 | ## Development Instructions 144 | 145 | ### Quickstart Development Instructions 146 | 147 | 1. [Fork and clone this repo](https://help.github.com/articles/fork-a-repo). 148 | 2. `cd` into the `esri-leaflet-vector` folder. 149 | 3. Install the dependencies with `npm install`. 150 | 4. Run `npm run dev` to compile the raw source inside a newly created `dist` folder and start up a development web server. 151 | - Alternatively, run `npm run start` to compile raw source code into both "debug" and "production" versions. This process will take longer to compile when saving your local changes to source code. Recommended only when building for production. 152 | 5. Open `examples/quickstart-dev.html` to see local changes in action. 153 | 154 | ### Advanced Development Instructions 155 | 156 | 1. [Fork and clone this repo](https://help.github.com/articles/fork-a-repo). 157 | 2. `cd` into the `esri-leaflet-vector` folder. 158 | 3. Install the dependencies with `npm install`. 159 | 4. Run `npm run build` to compile the raw source inside a newly created `dist` folder. 160 | 5. Run `npm test` from the command line to execute tests. 161 | 6. Open `examples/quickstart-dev.html` or `examples/quickstart-prod.html` to see local changes in action. 162 | 7. Create a [pull request](https://help.github.com/articles/creating-a-pull-request) if you'd like to share your work. 163 | 164 | ## Dependencies 165 | 166 | - Leaflet version [1.5.0](https://github.com/Leaflet/Leaflet/releases/tag/v1.5.0) (or higher) is required. 167 | - Esri Leaflet [2.3.0](https://github.com/Esri/esri-leaflet/releases/tag/v2.3.0) (or higher) is required. 168 | - [maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/) 169 | 170 | ## Resources 171 | 172 | - [ArcGIS for Developers](http://developers.arcgis.com) 173 | - [ArcGIS REST Services](http://resources.arcgis.com/en/help/arcgis-rest-api/) 174 | - [@Esri](http://twitter.com/esri) 175 | 176 | ## Issues 177 | 178 | Find a bug or want to request a new feature? Please let us know by submitting an [issue](https://github.com/Esri/esri-leaflet-vector/issues). 179 | 180 | Please take a look at previous issues on [Esri Leaflet](https://github.com/Esri/esri-leaflet-vector/issues?labels=FAQ&milestone=&page=1&state=closed) and Esri Leaflet [Vector](https://github.com/Esri/esri-leaflet-vector/issues) that resolve common problems. 181 | 182 | You can also post issues on the [GIS Stack Exchange](http://gis.stackexchange.com/questions/ask?tags=esri-leaflet,leaflet) an/or the [Esri Leaflet place](https://geonet.esri.com/discussion/create.jspa?sr=pmenu&containerID=1841&containerType=700&tags=esri-leaflet,leaflet) on GeoNet. 183 | 184 | ## Contributing 185 | 186 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/Esri/esri-leaflet/blob/master/CONTRIBUTING.md). 187 | 188 | ## [Terms](https://github.com/Esri/esri-leaflet#terms) 189 | 190 | ## Licensing 191 | 192 | Copyright © 2016-2020 Esri 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | > http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | 206 | A copy of the license is available in the repository's [LICENSE](./LICENSE) file. 207 | -------------------------------------------------------------------------------- /debug/README.md: -------------------------------------------------------------------------------- 1 | # Debug Examples 2 | 3 | This folder contains reproductions of issues that are difficult to test for in the unit tests: 4 | 5 | - `edge-of-the-world.html` reproduces [issue #130](https://github.com/Esri/esri-leaflet-vector/issues/130) 6 | -------------------------------------------------------------------------------- /debug/edge-of-the-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector Quickstart (PROD TEST) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 29 | 30 | 31 | 32 |
33 | 34 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/esri-leaflet-vector/bfbceed175c869a4e4297eeb4f038c5f3859a504/example.png -------------------------------------------------------------------------------- /examples/basemap-places.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector: Display basemap places 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 39 | 40 | 41 |
42 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/contour-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector Custom Vector Tile Layer 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 |
40 | 41 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/custom-vtl-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector Custom Vector Tile Layer 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 | 37 | 38 |
39 | 40 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/customize-basemap-style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector: Customize the basemap style 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 37 | 38 | 39 | 40 |
41 | 72 | 73 | -------------------------------------------------------------------------------- /examples/customize-vtl-style.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | Esri Leaflet: Customize a vector tile layer style 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 47 | 48 | 49 | 50 |
51 | 52 | 78 | 79 | -------------------------------------------------------------------------------- /examples/gallery-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector Basemap Gallery 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 |
40 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/languages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Tutorials: Change the basemap style 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 38 | 39 | 40 |
41 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /examples/open-basemaps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector Quickstart (DEV TEST) 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 | 37 | 38 |
39 | 40 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/osm-basemaps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector Quickstart (DEV TEST) 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 | 37 | 38 |
39 | 40 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/quickstart-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector Quickstart (DEV TEST) 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 |
40 | 41 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/quickstart-prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector Quickstart (PROD TEST) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 29 | 30 | 31 | 32 |
33 | 34 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/rtl-language.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Esri Leaflet Vector: RTL language support 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 39 | 40 | 41 | 42 |
43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/worldview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esri Leaflet Vector: Display basemap places 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 38 | 39 | 40 |
41 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'esri-leaflet-vector' { 2 | export function vectorBasemapLayer (key: any, options: any): any; 3 | export var VectorBasemapLayer: any; 4 | export function vectorTileLayer (key: any, options: any): any; 5 | export var VectorTileLayer: any; 6 | export function maplibreGLJSLayer (options: any): any; 7 | export var MaplibreGLJSLayer: any; 8 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri May 30 2014 15:44:45 GMT-0400 (EDT) 3 | 4 | module.exports = function (config) { 5 | const configuration = { 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha', 'sinon-chai'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'node_modules/leaflet/dist/leaflet.css', 17 | 'node_modules/leaflet/dist/leaflet-src.js', 18 | 'node_modules/esri-leaflet/dist/esri-leaflet-debug.js', 19 | 'dist/esri-leaflet-vector-debug.js', 20 | 'spec/**/*Spec.js' 21 | ], 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 28 | preprocessors: { 29 | 'dist/**/*.js': ['sourcemap', 'coverage'] 30 | }, 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress' 34 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 35 | reporters: ['mocha', 'coverage'], 36 | 37 | // web server port 38 | port: 9876, 39 | 40 | // level of logging 41 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 42 | logLevel: config.LOG_WARN, 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | // enable / disable watching file and executing tests whenever any file changes 48 | autoWatch: false, 49 | 50 | // start these browsers 51 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 52 | browsers: [ 53 | 'Chrome1280x1024' 54 | ], 55 | 56 | customLaunchers: { 57 | Chrome1280x1024: { 58 | base: 'ChromeHeadless', 59 | // increased viewport is required for some tests (TODO fix tests) 60 | // https://github.com/Leaflet/Leaflet/issues/7113#issuecomment-619528577 61 | flags: ['--window-size=1280,1024'] 62 | }, 63 | FirefoxTouch: { 64 | base: 'FirefoxHeadless', 65 | prefs: { 66 | 'dom.w3c_touch_events.enabled': 1 67 | } 68 | }, 69 | FirefoxNoTouch: { 70 | base: 'FirefoxHeadless', 71 | prefs: { 72 | 'dom.w3c_touch_events.enabled': 0 73 | } 74 | } 75 | }, 76 | 77 | concurrency: 1, 78 | 79 | // If browser does not capture in given timeout [ms], kill it 80 | captureTimeout: 60000, 81 | 82 | // Timeout for the client socket connection [ms]. 83 | browserSocketTimeout: 30000, 84 | 85 | // Continuous Integration mode 86 | // if true, Karma captures browsers, runs the tests and exits 87 | singleRun: true, 88 | 89 | client: { 90 | mocha: { 91 | // eslint-disable-next-line no-undef 92 | forbidOnly: process.env.CI || false 93 | } 94 | }, 95 | 96 | // Configure the coverage reporters 97 | coverageReporter: { 98 | reporters: [ 99 | { 100 | type: 'html', 101 | dir: 'coverage/' 102 | }, { 103 | type: 'text' 104 | } 105 | ] 106 | } 107 | }; 108 | 109 | config.set(configuration); 110 | }; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esri-leaflet-vector", 3 | "description": "Esri vector basemap and vector tile layer plugin for Leaflet.", 4 | "version": "4.3.0", 5 | "author": "John Gravois (https://johngravois.com)", 6 | "contributors": [ 7 | "Patrick Arlt (http://patrickarlt.com)", 8 | "Gavin Rehkemper (https://gavinr.com)", 9 | "Jacob Wasilkowski (https://jwasilgeo.github.io)", 10 | "George Owen (https://geowen.dev/)" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Esri/esri-leaflet-vector/issues" 14 | }, 15 | "peerDependencies": { 16 | "esri-leaflet": ">2.3.0", 17 | "leaflet": "^1.5.0", 18 | "maplibre-gl": "^2.0.0 || ^3.0.0 || ^4.0.0" 19 | }, 20 | "devDependencies": { 21 | "@rollup/plugin-commonjs": "^24.0.1", 22 | "@rollup/plugin-json": "^6.0.0", 23 | "@rollup/plugin-node-resolve": "^15.0.1", 24 | "@rollup/plugin-terser": "^0.3.0", 25 | "chai": "4.3.7", 26 | "chokidar-cli": "^3.0.0", 27 | "eslint": "^7.13.0", 28 | "eslint-config-semistandard": "^15.0.1", 29 | "eslint-config-standard": "^15.0.1", 30 | "eslint-plugin-chai-friendly": "^0.6.0", 31 | "eslint-plugin-import": "^2.22.1", 32 | "eslint-plugin-node": "^11.1.0", 33 | "eslint-plugin-promise": "^4.2.1", 34 | "eslint-plugin-standard": "^4.1.0", 35 | "esri-leaflet": "^3.0.0", 36 | "gh-release": "^7.0.2", 37 | "http-server": "^14.1.1", 38 | "karma": "^6.4.1", 39 | "karma-chrome-launcher": "^3.1.0", 40 | "karma-coverage": "^2.2.0", 41 | "karma-edgium-launcher": "github:matracey/karma-edgium-launcher", 42 | "karma-firefox-launcher": "^2.1.2", 43 | "karma-mocha": "^2.0.1", 44 | "karma-mocha-reporter": "^2.2.5", 45 | "karma-safari-launcher": "~1.0.0", 46 | "karma-sinon-chai": "^2.0.2", 47 | "karma-sourcemap-loader": "^0.3.8", 48 | "leaflet": "^1.5.0", 49 | "mkdirp": "^2.1.3", 50 | "mocha": "^10.2.0", 51 | "npm-run-all": "^4.1.5", 52 | "rollup": "^2.79.1", 53 | "semistandard": "^16.0.0", 54 | "sinon": "^15.0.1", 55 | "sinon-chai": "3.7.0", 56 | "snazzy": "^9.0.0" 57 | }, 58 | "files": [ 59 | "src/**/*.js", 60 | "dist/*.js", 61 | "dist/*.js.map", 62 | "dist/*.json", 63 | "index.d.ts" 64 | ], 65 | "homepage": "https://github.com/Esri/esri-leaflet-vector#readme", 66 | "jsnext:main": "src/EsriLeafletVector.js", 67 | "jspm": { 68 | "registry": "npm", 69 | "format": "es6", 70 | "main": "src/EsriLeafletVector.js" 71 | }, 72 | "keywords": [ 73 | "maplibre", 74 | "arcgis", 75 | "leaflet", 76 | "leafletjs", 77 | "maps" 78 | ], 79 | "license": "Apache-2.0", 80 | "main": "dist/esri-leaflet-vector-debug.js", 81 | "module": "src/EsriLeafletVector.js", 82 | "browser": "dist/esri-leaflet-vector-debug.js", 83 | "types": "index.d.ts", 84 | "readmeFilename": "README.md", 85 | "repository": { 86 | "type": "git", 87 | "url": "git+https://github.com/Esri/esri-leaflet-vector.git" 88 | }, 89 | "scripts": { 90 | "prebuild": "mkdirp dist", 91 | "build": "rollup -c profiles/debug.js & rollup -c profiles/production.js", 92 | "build-dev": "rollup -c profiles/debug.js", 93 | "fix": "semistandard --fix", 94 | "lint": "eslint src/**/*.js", 95 | "start-watch": "chokidar src -c \"npm run build\"", 96 | "start-watch-dev": "chokidar src -c \"npm run build-dev\"", 97 | "start": "run-p start-watch serve", 98 | "start-dev": "run-p start-watch-dev serve", 99 | "dev": "npm run start-dev", 100 | "serve": "http-server -p 8765 -c-1 -o", 101 | "pretest": "npm run build-dev", 102 | "test": "npm run lint && karma start", 103 | "release": "./scripts/release.sh" 104 | }, 105 | "semistandard": { 106 | "globals": [ 107 | "expect", 108 | "L", 109 | "XMLHttpRequest", 110 | "sinon", 111 | "xhr", 112 | "proj4" 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /profiles/base.js: -------------------------------------------------------------------------------- 1 | import config from '../node_modules/esri-leaflet/profiles/base.js'; 2 | 3 | config.input = 'src/EsriLeafletVector.js'; 4 | config.output.name = 'L.esri.Vector'; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /profiles/debug.js: -------------------------------------------------------------------------------- 1 | import config from './base.js'; 2 | 3 | // do not bundle maplibre-gl for dev/debug 4 | // otherwise build process is too slow during active development 5 | // dev sample pages must globally load maplibre-gl to work 6 | config.external.push('maplibre-gl'); 7 | config.output.globals['maplibre-gl'] = 'maplibregl'; 8 | 9 | config.output.file = 'dist/esri-leaflet-vector-debug.js'; 10 | config.output.sourcemap = true; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /profiles/production.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import config from './base.js'; 4 | 5 | config.output.file = 'dist/esri-leaflet-vector.js'; 6 | config.output.sourcemap = true; 7 | 8 | // use a Regex to preserve copyright text 9 | config.plugins.push(terser({ format: { comments: /Institute, Inc/ } })); 10 | 11 | // bundle maplibre-gl for production 12 | config.plugins.push( 13 | commonjs() 14 | ); 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # config 4 | VERSION=$(node --eval "console.log(require('./package.json').version);") 5 | NAME=$(node --eval "console.log(require('./package.json').name);") 6 | 7 | # build and test 8 | npm run test || exit 1 9 | 10 | # run build 11 | npm run build 12 | 13 | # Integrity string and save to siteData.json 14 | JS_INTEGRITY=$(cat dist/esri-leaflet-vector.js | openssl dgst -sha512 -binary | openssl base64 -A) 15 | echo "{\"name\": \"esri-leaflet-vector\",\"version\": \"$VERSION\",\"lib\": {\"path\": \"dist/esri-leaflet-vector.js\",\"integrity\": \"sha512-$JS_INTEGRITY\"}}" > dist/siteData.json 16 | 17 | # checkout temp branch for release 18 | git checkout -b gh-release 19 | 20 | # force add files 21 | git add dist -f 22 | 23 | # commit changes with a versioned commit message 24 | git commit -m "build $VERSION" 25 | 26 | # push commit so it exists on GitHub when we run gh-release 27 | git push git@github.com:Esri/esri-leaflet-vector.git gh-release 28 | 29 | # create a ZIP archive of the dist files 30 | zip -r $NAME-v$VERSION.zip dist 31 | 32 | # run gh-release to create the tag and push release to github 33 | # may need to run this instead on Windows: ./node_modules/.bin/gh-release --assets $NAME-v$VERSION.zip 34 | gh-release --assets $NAME-v$VERSION.zip 35 | 36 | # publish release on NPM 37 | npm publish 38 | 39 | # checkout master and delete release branch locally and on GitHub 40 | git checkout master 41 | git branch -D gh-release 42 | git push git@github.com:Esri/esri-leaflet-vector.git :gh-release 43 | -------------------------------------------------------------------------------- /spec/UtilSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const metadata = { 3 | tiles: ['tile/{z}/{y}/{x}.pbf'], 4 | tileInfo: { 5 | lods: [ 6 | { 7 | level: 0, 8 | resolution: 78271.516964, 9 | scale: 295828763.7957775 10 | } 11 | ] 12 | } 13 | }; 14 | 15 | describe('Util', function () { 16 | it('should include the token in the sprite URL when the sprite URL is relative', function () { 17 | const spriteUrl = '../sprites/sprite'; 18 | const token = 'asdf'; 19 | const styleUrl = 20 | 'https://tiles.arcgis.com/tiles/test/arcgis/rest/services/test/VectorTileServer/resources/styles/root.json'; 21 | const fullSpriteUrl = 22 | 'https://tiles.arcgis.com/tiles/test/arcgis/rest/services/test/VectorTileServer/resources/sprites/sprite'; 23 | 24 | const style = L.esri.Vector.Util.formatStyle( 25 | { 26 | version: 8, 27 | sprite: spriteUrl, 28 | glyphs: '../fonts/{fontstack}/{range}.pbf', 29 | sources: { 30 | esri: { 31 | type: 'vector', 32 | attribution: 'test', 33 | bounds: [-180, -85.0511, 180, 85.0511], 34 | minzoom: 0, 35 | maxzoom: 19, 36 | scheme: 'xyz', 37 | url: '../../' 38 | } 39 | }, 40 | layers: [] 41 | }, 42 | styleUrl, 43 | metadata, 44 | token 45 | ); 46 | 47 | // console.log("style.sprite", style.sprite); 48 | expect(style.sprite).to.equal(`${fullSpriteUrl}?token=${token}`); 49 | }); 50 | 51 | it('should include the token in the sprite URL when the sprite URL starts with https', function () { 52 | const styleUrl = 53 | 'https://cdn.arcgis.com/sharing/rest/content/items/asdf/resources/styles/root.json'; 54 | const token = 'asdf'; 55 | 56 | const style = L.esri.Vector.Util.formatStyle( 57 | { 58 | version: 8, 59 | sprite: 60 | 'https://www.arcgis.com/sharing/rest/content/items/123456789/resources/sprites/sprite-1679474043120', 61 | glyphs: '../fonts/{fontstack}/{range}.pbf', 62 | sources: { 63 | esri: { 64 | type: 'vector', 65 | attribution: 'test', 66 | bounds: [-180, -85.0511, 180, 85.0511], 67 | minzoom: 0, 68 | maxzoom: 19, 69 | scheme: 'xyz', 70 | url: '../../' 71 | } 72 | }, 73 | layers: [] 74 | }, 75 | styleUrl, 76 | metadata, 77 | token 78 | ); 79 | 80 | expect(style.sprite).to.equal( 81 | `https://cdn.arcgis.com/sharing/rest/content/items/123456789/resources/sprites/sprite-1679474043120?token=${token}` 82 | ); 83 | }); 84 | 85 | it('should include the token in the glyph URL when the glyph URL is relative', function () { 86 | const token = 'asdf'; 87 | const glyphUrl = '../fonts/{fontstack}/{range}.pbf'; 88 | const styleUrl = 89 | 'https://tiles.arcgis.com/tiles/test/arcgis/rest/services/test/VectorTileServer/resources/styles/root.json'; 90 | const fullGlyphUrl = 91 | 'https://tiles.arcgis.com/tiles/test/arcgis/rest/services/test/VectorTileServer/resources/fonts/{fontstack}/{range}.pbf'; 92 | 93 | const style = L.esri.Vector.Util.formatStyle( 94 | { 95 | version: 8, 96 | sprite: 97 | 'https://www.arcgis.com/sharing/rest/content/items/123456789/resources/sprites/sprite-1679474043120', 98 | glyphs: glyphUrl, 99 | sources: { 100 | esri: { 101 | type: 'vector', 102 | attribution: 'test', 103 | bounds: [-180, -85.0511, 180, 85.0511], 104 | minzoom: 0, 105 | maxzoom: 19, 106 | scheme: 'xyz', 107 | url: '../../' 108 | } 109 | }, 110 | layers: [] 111 | }, 112 | styleUrl, 113 | metadata, 114 | token 115 | ); 116 | 117 | expect(style.glyphs).to.equal(`${fullGlyphUrl}?token=${token}`); 118 | }); 119 | 120 | it('should include the token in the glyph URL when the glyph URL starts with https', function () { 121 | const token = 'asdf'; 122 | const styleUrl = 123 | 'https://cdn.arcgis.com/sharing/rest/content/items/asdf/resources/styles/root.json'; 124 | 125 | const style = L.esri.Vector.Util.formatStyle( 126 | { 127 | version: 8, 128 | sprite: 129 | 'https://www.arcgis.com/sharing/rest/content/items/123456789/resources/sprites/sprite-1679474043120', 130 | glyphs: 131 | 'https://www.arcgis.com/sharing/rest/content/items/123456789/resources/fonts/{fontstack}/{range}.pbf', 132 | sources: { 133 | esri: { 134 | type: 'vector', 135 | attribution: 'test', 136 | bounds: [-180, -85.0511, 180, 85.0511], 137 | minzoom: 0, 138 | maxzoom: 19, 139 | scheme: 'xyz', 140 | url: '../../' 141 | } 142 | }, 143 | layers: [] 144 | }, 145 | styleUrl, 146 | metadata, 147 | token 148 | ); 149 | 150 | expect(style.glyphs).to.equal( 151 | `https://cdn.arcgis.com/sharing/rest/content/items/123456789/resources/fonts/{fontstack}/{range}.pbf?token=${token}` 152 | ); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /spec/VectorBasemapLayerSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const itemId = '287c07ef752246d08bb4712fd4b74438'; 3 | const apikey = '1234'; 4 | const basemapKey = 'ArcGIS:Streets'; 5 | const basemapKeyV2 = 'arcgis/streets'; 6 | const customBasemap = 'f04f33b9626240f084cb52f0b08758ef'; 7 | const language = 'zh_s'; 8 | const worldview = 'morocco'; 9 | const places = 'attributed'; 10 | 11 | describe('VectorBasemapLayer', function () { 12 | it('should have a L.esri.vectorBasemapLayer alias', function () { 13 | console.log( 14 | 'L.esri.Vector.vectorBasemapLayer', 15 | L.esri.Vector.vectorBasemapLayer 16 | ); 17 | 18 | expect( 19 | L.esri.Vector.vectorBasemapLayer(itemId, { 20 | apikey: apikey 21 | }) 22 | ).to.be.instanceof(L.esri.Vector.VectorBasemapLayer); 23 | }); 24 | 25 | it('should save the key from the constructor - itemID', function () { 26 | const layer = L.esri.Vector.vectorBasemapLayer(itemId, { 27 | apikey: apikey 28 | }); 29 | 30 | expect(layer.options.key).to.equal(itemId); 31 | }); 32 | 33 | it('should error if no api key or token', function () { 34 | expect(function () { 35 | L.esri.Vector.vectorBasemapLayer(basemapKey, {}); 36 | }).to.throw('API Key or token is required for vectorBasemapLayer.'); 37 | }); 38 | 39 | it('should save the key from the constructor - enumeration basemap key', function () { 40 | const layer = L.esri.Vector.vectorBasemapLayer(basemapKey, { 41 | apikey: apikey 42 | }); 43 | 44 | expect(layer.options.key).to.equal(basemapKey); 45 | }); 46 | 47 | it('should save the api key from the constructor', function () { 48 | const layer = L.esri.Vector.vectorBasemapLayer(basemapKey, { 49 | apikey: apikey 50 | }); 51 | 52 | expect(layer.options.apikey).to.equal(apikey); 53 | }); 54 | 55 | it('should save the token as apikey from the constructor', function () { 56 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKey, { 57 | token: apikey 58 | }); 59 | 60 | expect(layer.options.apikey).to.equal(apikey); 61 | }); 62 | 63 | it("should create basemap styles in the 'tilePane' by default", function () { 64 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKey, { 65 | apikey: apikey 66 | }); 67 | const layerV2 = new L.esri.Vector.VectorBasemapLayer(basemapKeyV2, { 68 | apikey: apikey, 69 | version: 2 70 | }); 71 | 72 | expect(layer.options.pane).to.equal('tilePane'); 73 | expect(layerV2.options.pane).to.equal('tilePane'); 74 | }); 75 | 76 | it("should add 'Labels' styles to the 'esri-labels' pane by default", function () { 77 | const layer = new L.esri.Vector.VectorBasemapLayer( 78 | 'ArcGIS:Imagery:Labels', 79 | { 80 | apikey: apikey 81 | } 82 | ); 83 | 84 | const layerV2 = new L.esri.Vector.VectorBasemapLayer( 85 | 'arcgis/imagery/labels', 86 | { 87 | apikey: apikey, 88 | version: 2 89 | } 90 | ); 91 | 92 | // These label styles use a different endpoint (/label instead of /labels, for some reason) 93 | const humanGeoLayer = new L.esri.Vector.VectorBasemapLayer( 94 | 'ArcGIS:HumanGeography:Label', 95 | { 96 | apikey: apikey 97 | } 98 | ); 99 | const humanGeoLayerV2 = new L.esri.Vector.VectorBasemapLayer( 100 | 'arcgis/human-geography/label', 101 | { 102 | apikey: apikey, 103 | version: 2 104 | } 105 | ); 106 | 107 | expect(layer.options.pane).to.equal('esri-labels'); 108 | expect(layerV2.options.pane).to.equal('esri-labels'); 109 | expect(humanGeoLayer.options.pane).to.equal('esri-labels'); 110 | expect(humanGeoLayerV2.options.pane).to.equal('esri-labels'); 111 | }); 112 | 113 | it("should add 'Detail' styles to the 'esri-detail' pane by default", function () { 114 | const layer = new L.esri.Vector.VectorBasemapLayer( 115 | 'ArcGIS:Terrain:Detail', 116 | { 117 | apikey: apikey 118 | } 119 | ); 120 | const layerV2 = new L.esri.Vector.VectorBasemapLayer( 121 | 'arcgis/terrain/detail', 122 | { 123 | apikey: apikey, 124 | version: 2 125 | } 126 | ); 127 | 128 | expect(layer.options.pane).to.equal('esri-detail'); 129 | expect(layerV2.options.pane).to.equal('esri-detail'); 130 | }); 131 | 132 | it('should save the service version from the constructor', function () { 133 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKeyV2, { 134 | apikey: apikey, 135 | version: 2 136 | }); 137 | 138 | expect(layer.options.version).to.equal(2); 139 | }); 140 | 141 | it('should load a v1 basemap from a v1 style key without needing to specify a version', function () { 142 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKey, { 143 | apikey: apikey 144 | }); 145 | 146 | expect(layer.options.version).to.equal(1); 147 | }); 148 | 149 | it('should load a v2 basemap from a v2 style key without needing to specify a version', function () { 150 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKeyV2, { 151 | apikey: apikey 152 | }); 153 | 154 | expect(layer.options.version).to.equal(2); 155 | }); 156 | 157 | it('should save the language and worldview parameters from the constructor', function () { 158 | const layer = new L.esri.Vector.VectorBasemapLayer(basemapKeyV2, { 159 | apikey: apikey, 160 | version: 2, 161 | language: language, 162 | worldview: worldview, 163 | places: places 164 | }); 165 | 166 | expect(layer.options.language).to.equal(language); 167 | expect(layer.options.worldview).to.equal(worldview); 168 | expect(layer.options.places).to.equal(places); 169 | }); 170 | 171 | it('should error if a language is provided when accessing the v1 service', function () { 172 | expect(function () { 173 | L.esri.Vector.vectorBasemapLayer(basemapKey, { 174 | apikey: apikey, 175 | language: language 176 | }); 177 | }).to.throw( 178 | 'The language parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 179 | ); 180 | }); 181 | 182 | it('should error if a worldview is provided when accessing the v1 service', function () { 183 | expect(function () { 184 | L.esri.Vector.vectorBasemapLayer(basemapKey, { 185 | apikey: apikey, 186 | worldview: worldview 187 | }); 188 | }).to.throw( 189 | 'The worldview parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 190 | ); 191 | }); 192 | 193 | it('should error if a places parameter is provided when accessing the v1 service', function () { 194 | expect(function () { 195 | L.esri.Vector.vectorBasemapLayer(basemapKey, { 196 | apikey: apikey, 197 | places: places 198 | }); 199 | }).to.throw( 200 | 'The places parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 201 | ); 202 | }); 203 | 204 | it('should not accept a v2 style enumeration when accessing the v1 service', function () { 205 | expect(function () { 206 | L.esri.Vector.vectorBasemapLayer(basemapKeyV2, { 207 | apikey: apikey, 208 | version: 1 209 | }); 210 | }).to.throw( 211 | basemapKeyV2 + 212 | ' is a v2 style enumeration. Set version:2 to request this style' 213 | ); 214 | }); 215 | 216 | it('should not accept a v1 style enumeration when accessing the v2 service', function () { 217 | expect(function () { 218 | L.esri.Vector.vectorBasemapLayer(basemapKey, { 219 | apikey: apikey, 220 | version: 2 221 | }); 222 | }).to.throw( 223 | basemapKey + 224 | ' is a v1 style enumeration. Set version:1 to request this style' 225 | ); 226 | }); 227 | 228 | it('should load a custom basemap style from an item ID when using the v1 service', function () { 229 | const customLayer = L.esri.Vector.vectorBasemapLayer(customBasemap, { 230 | apikey: apikey, 231 | version: 1 232 | }); 233 | expect(customLayer._maplibreGL.options.style).to.equal( 234 | `https://basemaps-api.arcgis.com/arcgis/rest/services/styles/${customBasemap}?type=style&token=${apikey}` 235 | ); 236 | }); 237 | 238 | it('should load a custom basemap style from an item ID when using the v2 service', function () { 239 | const customLayer = L.esri.Vector.vectorBasemapLayer(customBasemap, { 240 | apikey: apikey, 241 | version: 2 242 | }); 243 | expect(customLayer._maplibreGL.options.style).to.equal( 244 | `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/items/${customBasemap}?token=${apikey}` 245 | ); 246 | }); 247 | 248 | it('should error if a language is provided when loading a custom basemap style', function () { 249 | expect(function () { 250 | L.esri.Vector.vectorBasemapLayer(customBasemap, { 251 | apikey, 252 | version: 2, 253 | language: language 254 | }); 255 | }).to.throw( 256 | "The 'language' parameter is not supported for custom basemap styles" 257 | ); 258 | }); 259 | 260 | describe('_getAttributionUrls', function () { 261 | it('should handle OSM keys', function () { 262 | const key = 'OSM:Standard'; 263 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 264 | token: apikey 265 | }); 266 | const attributionUrls = layer._getAttributionUrls(key); 267 | expect(attributionUrls.length).to.equal(1); 268 | expect(attributionUrls[0]).to.equal( 269 | 'https://static.arcgis.com/attribution/Vector/OpenStreetMap_v2' 270 | ); 271 | }); 272 | 273 | it('should handle ArcGIS Imagery keys', function () { 274 | const key = 'ArcGIS:Imagery'; 275 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 276 | token: apikey 277 | }); 278 | const attributionUrls = layer._getAttributionUrls(key); 279 | expect(attributionUrls.length).to.equal(2); 280 | expect(attributionUrls[0]).to.equal( 281 | 'https://static.arcgis.com/attribution/World_Imagery' 282 | ); 283 | expect(attributionUrls[1]).to.equal( 284 | 'https://static.arcgis.com/attribution/Vector/World_Basemap_v2' 285 | ); 286 | }); 287 | 288 | it('should handle ArcGIS non-Imagery keys', function () { 289 | const key = 'ArcGIS:Streets'; 290 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 291 | token: apikey 292 | }); 293 | const attributionUrls = layer._getAttributionUrls(key); 294 | expect(attributionUrls.length).to.equal(1); 295 | expect(attributionUrls[0]).to.equal( 296 | 'https://static.arcgis.com/attribution/Vector/World_Basemap_v2' 297 | ); 298 | }); 299 | }); 300 | 301 | describe('_setupAttribution', function () { 302 | it('should add attribution for non itemId item', function () { 303 | const key = 'ArcGIS:Streets'; 304 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 305 | token: apikey 306 | }); 307 | layer._ready = false; 308 | let attributionValue = ''; 309 | const fakeMap = { 310 | attributionControl: { 311 | setPrefix: function () {}, 312 | _container: { className: '', querySelector: () => {} }, 313 | addAttribution: function () { 314 | attributionValue = arguments[0]; 315 | } 316 | }, 317 | getSize: function () { 318 | return { x: 0, y: 0 }; 319 | }, 320 | on: function () {} 321 | }; 322 | layer.onAdd(fakeMap); 323 | layer._setupAttribution(); 324 | expect(attributionValue).to.be.equal( 325 | '' 326 | ); 327 | }); 328 | 329 | it('should add attribution for itemId item', function () { 330 | const key = '3e1a00aeae81496587988075fe529f71'; 331 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 332 | token: apikey 333 | }); 334 | layer._ready = false; 335 | let attributionValue = '?'; 336 | const fakeMap = { 337 | attributionControl: { 338 | setPrefix: function () {}, 339 | _container: { className: '', querySelector: () => {} }, 340 | addAttribution: function () { 341 | attributionValue = arguments[0]; 342 | } 343 | }, 344 | getSize: function () { 345 | return { x: 0, y: 0 }; 346 | }, 347 | on: function () {} 348 | }; 349 | layer.onAdd(fakeMap); 350 | layer._maplibreGL.getMaplibreMap = function () { 351 | return { 352 | style: { 353 | stylesheet: { 354 | sources: { 355 | one: { 356 | attribution: '@ my attribution', 357 | copyrightText: '@ my copyright text' 358 | } 359 | } 360 | } 361 | } 362 | }; 363 | }; 364 | 365 | layer._setupAttribution(); 366 | const expectedAttributionValue = 367 | 'Powered by Esri | @ my attribution, @ my copyright text'; 368 | expect(attributionValue).to.be.equal(expectedAttributionValue); 369 | }); 370 | }); 371 | 372 | describe('onRemove', function () { 373 | it('should call esri-leaflet and attributionControl remove attribution methods', function () { 374 | const key = 'ArcGIS:Streets'; 375 | const layer = new L.esri.Vector.VectorBasemapLayer(key, { 376 | token: apikey 377 | }); 378 | layer._ready = false; 379 | const fakeMap = { 380 | attributionControl: { 381 | removeAttribution: function () {} 382 | }, 383 | off: function () {}, 384 | removeLayer: function () {} 385 | }; 386 | 387 | const attributionControlSpy = sinon.spy(fakeMap.attributionControl); 388 | const utilSpy = sinon.spy(L.esri.Util, 'removeEsriAttribution'); 389 | 390 | sinon.stub(document, 'getElementsByClassName').callsFake(function () { 391 | return [{ outerHTML: '
' }]; 392 | }); 393 | 394 | layer.onRemove(fakeMap); 395 | document.getElementsByClassName.restore(); 396 | 397 | expect(utilSpy.calledWith(fakeMap)).to.be.equal(true); 398 | expect(attributionControlSpy.removeAttribution.callCount).to.be.equal(2); 399 | }); 400 | }); 401 | }); 402 | -------------------------------------------------------------------------------- /spec/VectorTileLayerSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | describe('VectorTileLayer', function () { 4 | // These must be vars (instead of const) due to how the unit tests are run: 5 | const itemId = '1c365daf37a744fbad748b67aa69dac8'; 6 | const apikey = 'dcba4321'; 7 | const serviceUrl = 8 | 'https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Microsoft_Building_Footprints/VectorTileServer'; 9 | const token = '1234abcd'; 10 | let server; 11 | 12 | // for layers hosted in ArcGIS Enterprise instead of ArcGIS Online 13 | const onPremisePortalUrl = 'https://PATH/TO/ARCGIS/ENTERPRISE'; // defaults to https://www.arcgis.com 14 | const onPremiseItemId = '1c365daf37a744fbad748b67aa69dac8'; 15 | const onPremiseServiceUrl = 16 | 'https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Microsoft_Building_Footprints/VectorTileServer'; 17 | 18 | beforeEach(function () { 19 | server = sinon.fakeServer.create(); 20 | }); 21 | 22 | afterEach(function () { 23 | server.restore(); 24 | sinon.restore(); 25 | }); 26 | 27 | it('should have a L.esri.vectorTileLayer alias', function () { 28 | console.log('L.esri.Vector.vectorTileLayer', L.esri.Vector.vectorTileLayer); 29 | 30 | expect(L.esri.Vector.vectorTileLayer(itemId)).to.be.instanceof( 31 | L.esri.Vector.VectorTileLayer 32 | ); 33 | }); 34 | 35 | it('should save the key from the constructor - itemId', function () { 36 | const layer = L.esri.Vector.vectorTileLayer(itemId); 37 | 38 | expect(layer.options.key).to.equal(itemId); 39 | }); 40 | 41 | it('should save the key from the constructor - serviceUrl', function () { 42 | const layer = L.esri.Vector.vectorTileLayer(serviceUrl); 43 | 44 | expect(layer.options.key).to.equal(serviceUrl); 45 | }); 46 | 47 | it('should error if no key itemId or serviceUrl', function () { 48 | expect(function () { 49 | L.esri.Vector.vectorTileLayer(); 50 | }).to.throw('An ITEM ID or SERVICE URL is required for vectorTileLayer.'); 51 | 52 | expect(function () { 53 | L.esri.Vector.vectorTileLayer(false, {}); 54 | }).to.throw('An ITEM ID or SERVICE URL is required for vectorTileLayer.'); 55 | }); 56 | 57 | it('should save the token from the constructor', function () { 58 | const layer = new L.esri.Vector.VectorTileLayer(itemId, { 59 | token: token 60 | }); 61 | 62 | expect(layer.options.token).to.equal(token); 63 | }); 64 | 65 | it('should save the api key as token from the constructor', function () { 66 | const layer = L.esri.Vector.vectorTileLayer(itemId, { 67 | apikey: apikey 68 | }); 69 | 70 | expect(layer.options.token).to.equal(apikey); 71 | }); 72 | 73 | it('should default to the "overlayPane"', function () { 74 | const layer = L.esri.Vector.vectorTileLayer(itemId); 75 | 76 | expect(layer.options.pane).to.equal('overlayPane'); 77 | }); 78 | 79 | it('should let the default pane be changed in the constructor', function () { 80 | const otherPane = 'shadowPane'; 81 | const layer = L.esri.Vector.vectorTileLayer(itemId, { 82 | pane: otherPane 83 | }); 84 | 85 | expect(layer.options.pane).to.equal(otherPane); 86 | }); 87 | 88 | it('should default to ArcGIS Online as the base "portalUrl" for loading the style - itemId', function () { 89 | const layer = L.esri.Vector.vectorTileLayer(itemId); 90 | 91 | expect(layer.options.portalUrl).to.equal('https://www.arcgis.com'); 92 | }); 93 | 94 | it('should default to ArcGIS Online as the base "portalUrl" for loading the style - serviceUrl', function () { 95 | const layer = L.esri.Vector.vectorTileLayer(serviceUrl); 96 | 97 | expect(layer.options.portalUrl).to.equal('https://www.arcgis.com'); 98 | }); 99 | 100 | it('should let the base "portalUrl" be changed in the constructor for loading an on-premise style - itemId', function () { 101 | const layer = L.esri.Vector.vectorTileLayer(onPremiseItemId, { 102 | portalUrl: onPremisePortalUrl 103 | }); 104 | 105 | expect(layer.options.portalUrl).to.equal(onPremisePortalUrl); 106 | }); 107 | 108 | it('should let the base "portalUrl" be changed in the constructor for loading an on-premise style - serviceUrl', function () { 109 | const layer = L.esri.Vector.vectorTileLayer(onPremiseServiceUrl, { 110 | portalUrl: onPremisePortalUrl 111 | }); 112 | 113 | expect(layer.options.portalUrl).to.equal(onPremisePortalUrl); 114 | }); 115 | 116 | it('should emit load-error for invalid itemID', function (done) { 117 | server.respondWith( 118 | 'GET', 119 | 'https://esri.maps.arcgis.com/sharing/rest/content/items/75f4dfdff19e445395653121a95a85db_WRONG/resources/styles/root.json?f=json', 120 | JSON.stringify({ 121 | error: { 122 | code: 400, 123 | messageCode: 'CONT_0001', 124 | message: 'Item does not exist or is inaccessible.', 125 | details: [] 126 | } 127 | }) 128 | ); 129 | 130 | server.respondWith( 131 | 'GET', 132 | 'https://esri.maps.arcgis.com/sharing/rest/content/items/75f4dfdff19e445395653121a95a85db_WRONG?f=json', 133 | JSON.stringify({ 134 | error: { 135 | code: 400, 136 | messageCode: 'CONT_0001', 137 | message: 'Item does not exist or is inaccessible.', 138 | details: [] 139 | } 140 | }) 141 | ); 142 | 143 | const layer = new L.esri.Vector.VectorTileLayer( 144 | '75f4dfdff19e445395653121a95a85db_WRONG', 145 | { 146 | portalUrl: 'https://esri.maps.arcgis.com' 147 | } 148 | ); 149 | layer.on('load-error', function (e) { 150 | expect(e.type).to.equal('load-error'); 151 | done(); 152 | }); 153 | server.respond(); 154 | server.respond(); 155 | }); 156 | 157 | it('should emit load-error for bad service url', function (done) { 158 | server.respondWith( 159 | 'GET', 160 | 'https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Microsoft_Building_Footprints_WRONG/VectorTileServer?f=json', 161 | JSON.stringify({ 162 | error: { 163 | code: 404, 164 | message: 'Requested Service not available.', 165 | details: null 166 | } 167 | }) 168 | ); 169 | 170 | const layer = new L.esri.Vector.VectorTileLayer( 171 | 'https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Microsoft_Building_Footprints_WRONG/VectorTileServer' 172 | ); 173 | layer.on('load-error', function (e) { 174 | expect(e.type).to.equal('load-error'); 175 | done(); 176 | }); 177 | server.respond(); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/EsriLeafletVector.js: -------------------------------------------------------------------------------- 1 | // export version 2 | import packageInfo from '../package.json'; 3 | const version = packageInfo.version; 4 | export { version as VERSION }; 5 | 6 | export { VectorBasemapLayer, vectorBasemapLayer } from './VectorBasemapLayer'; 7 | export { VectorTileLayer, vectorTileLayer } from './VectorTileLayer'; 8 | export { EsriUtil as Util } from './Util'; 9 | export { MaplibreGLJSLayer, maplibreGLJSLayer, setRTLTextPlugin } from './MaplibreGLLayer'; 10 | -------------------------------------------------------------------------------- /src/MaplibreGLLayer.js: -------------------------------------------------------------------------------- 1 | import { 2 | DomEvent, 3 | DomUtil, 4 | extend, 5 | latLngBounds, 6 | Layer, 7 | setOptions, 8 | Util 9 | } from 'leaflet'; 10 | import maplibregl from 'maplibre-gl'; 11 | 12 | export const setRTLTextPlugin = (url, callback, deferred) => { 13 | maplibregl.setRTLTextPlugin(url, callback, deferred); 14 | }; 15 | 16 | export const MaplibreGLJSLayer = Layer.extend({ 17 | options: { 18 | updateInterval: 32, 19 | // How much to extend the overlay view (relative to map size) 20 | // e.g. 0.1 would be 10% of map view in each direction 21 | padding: 0.1, 22 | // whether or not to register the mouse and keyboard 23 | // events on the mapbox overlay 24 | interactive: false, 25 | // set the tilepane as the default pane to draw gl tiles 26 | pane: 'tilePane' 27 | }, 28 | 29 | initialize: function (options) { 30 | setOptions(this, options); 31 | 32 | // setup throttling the update event when panning 33 | this._throttledUpdate = Util.throttle( 34 | this._update, 35 | this.options.updateInterval, 36 | this 37 | ); 38 | }, 39 | 40 | onAdd: function (map) { 41 | if (!this._container) { 42 | this._initContainer(); 43 | } 44 | 45 | const paneName = this.getPaneName(); 46 | map.getPane(paneName).appendChild(this._container); 47 | 48 | this._initGL(); 49 | 50 | this._offset = this._map.containerPointToLayerPoint([0, 0]); 51 | 52 | // work around https://github.com/mapbox/mapbox-gl-leaflet/issues/47 53 | if (map.options.zoomAnimation) { 54 | DomEvent.on( 55 | map._proxy, 56 | DomUtil.TRANSITION_END, 57 | this._transitionEnd, 58 | this 59 | ); 60 | } 61 | }, 62 | 63 | onRemove: function (map) { 64 | if (this._map._proxy && this._map.options.zoomAnimation) { 65 | DomEvent.off( 66 | this._map._proxy, 67 | DomUtil.TRANSITION_END, 68 | this._transitionEnd, 69 | this 70 | ); 71 | } 72 | 73 | const paneName = this.getPaneName(); 74 | 75 | map.getPane(paneName).removeChild(this._container); 76 | this._container = null; 77 | 78 | this._glMap.remove(); 79 | this._glMap = null; 80 | }, 81 | 82 | getEvents: function () { 83 | return { 84 | move: this._throttledUpdate, // sensibly throttle updating while panning 85 | zoomanim: this._animateZoom, // applies the zoom animation to the 86 | zoom: this._pinchZoom, // animate every zoom event for smoother pinch-zooming 87 | zoomstart: this._zoomStart, // flag starting a zoom to disable panning 88 | zoomend: this._zoomEnd, 89 | resize: this._resize 90 | }; 91 | }, 92 | 93 | getMaplibreMap: function () { 94 | return this._glMap; 95 | }, 96 | 97 | getCanvas: function () { 98 | return this._glMap.getCanvas(); 99 | }, 100 | 101 | getSize: function () { 102 | return this._map.getSize().multiplyBy(1 + this.options.padding * 2); 103 | }, 104 | 105 | getOpacity: function () { 106 | return this.options.opacity; 107 | }, 108 | 109 | setOpacity: function (opacity) { 110 | this.options.opacity = opacity; 111 | this._container.style.opacity = opacity; 112 | }, 113 | 114 | getBounds: function () { 115 | const halfSize = this.getSize().multiplyBy(0.5); 116 | const center = this._map.latLngToContainerPoint(this._map.getCenter()); 117 | return latLngBounds( 118 | this._map.containerPointToLatLng(center.subtract(halfSize)), 119 | this._map.containerPointToLatLng(center.add(halfSize)) 120 | ); 121 | }, 122 | 123 | getContainer: function () { 124 | return this._container; 125 | }, 126 | 127 | // returns the pane name set in options if it is a valid pane, defaults to tilePane 128 | getPaneName: function () { 129 | return this._map.getPane(this.options.pane) 130 | ? this.options.pane 131 | : 'tilePane'; 132 | }, 133 | 134 | _resize: function () { 135 | return this._glMap._resize; 136 | }, 137 | 138 | _initContainer: function () { 139 | if (this._container) { 140 | return; 141 | } 142 | 143 | this._container = DomUtil.create('div', 'leaflet-gl-layer'); 144 | 145 | const size = this.getSize(); 146 | const offset = this._map.getSize().multiplyBy(this.options.padding); 147 | this._container.style.width = size.x + 'px'; 148 | this._container.style.height = size.y + 'px'; 149 | 150 | const topLeft = this._map 151 | .containerPointToLayerPoint([0, 0]) 152 | .subtract(offset); 153 | 154 | DomUtil.setPosition(this._container, topLeft); 155 | }, 156 | 157 | _initGL: function () { 158 | if (this._glMap) { 159 | return; 160 | } 161 | 162 | const center = this._map.getCenter(); 163 | 164 | const options = extend({}, this.options, { 165 | container: this._container, 166 | center: [center.lng, center.lat], 167 | zoom: this._map.getZoom() - 1, 168 | attributionControl: false 169 | }); 170 | 171 | this._glMap = new maplibregl.Map(options); 172 | 173 | // Listen for style data error (401 Unauthorized) 174 | this._glMap.on('error', function (error) { 175 | if (error.error && error.error.status === 401) { 176 | console.warn( 177 | 'Invalid or expired API key. Please check that API key is not expired and has the basemaps privilege assigned.' 178 | ); 179 | } 180 | }); 181 | 182 | // Fire event for Maplibre "styledata" event. 183 | this._glMap.once( 184 | 'styledata', 185 | function (res) { 186 | this.fire('styleLoaded'); 187 | }.bind(this) 188 | ); 189 | 190 | // allow GL base map to pan beyond min/max latitudes 191 | this._glMap.transform.latRange = null; 192 | this._glMap.transform.maxValidLatitude = Infinity; 193 | 194 | this._transformGL(this._glMap); 195 | 196 | if (this._glMap._canvas.canvas) { 197 | // older versions of mapbox-gl surfaced the canvas differently 198 | this._glMap._actualCanvas = this._glMap._canvas.canvas; 199 | } else { 200 | this._glMap._actualCanvas = this._glMap._canvas; 201 | } 202 | 203 | // treat child element like L.ImageOverlay 204 | const canvas = this._glMap._actualCanvas; 205 | DomUtil.addClass(canvas, 'leaflet-image-layer'); 206 | DomUtil.addClass(canvas, 'leaflet-zoom-animated'); 207 | if (this.options.interactive) { 208 | DomUtil.addClass(canvas, 'leaflet-interactive'); 209 | } 210 | if (this.options.className) { 211 | DomUtil.addClass(canvas, this.options.className); 212 | } 213 | }, 214 | 215 | _update: function (e) { 216 | // update the offset, so we can correct for it later when we zoom 217 | this._offset = this._map.containerPointToLayerPoint([0, 0]); 218 | 219 | if (this._zooming) { 220 | return; 221 | } 222 | 223 | const size = this.getSize(); 224 | const container = this._container; 225 | const gl = this._glMap; 226 | const offset = this._map.getSize().multiplyBy(this.options.padding); 227 | const topLeft = this._map 228 | .containerPointToLayerPoint([0, 0]) 229 | .subtract(offset); 230 | 231 | DomUtil.setPosition(container, topLeft); 232 | 233 | this._transformGL(gl); 234 | 235 | if (gl.transform.width !== size.x || gl.transform.height !== size.y) { 236 | container.style.width = size.x + 'px'; 237 | container.style.height = size.y + 'px'; 238 | if (gl._resize !== null && gl._resize !== undefined) { 239 | gl._resize(); 240 | } else { 241 | gl.resize(); 242 | } 243 | } else { 244 | // older versions of mapbox-gl surfaced update publicly 245 | if (gl._update !== null && gl._update !== undefined) { 246 | gl._update(); 247 | } else { 248 | gl.update(); 249 | } 250 | } 251 | }, 252 | 253 | _transformGL: function (gl) { 254 | const center = this._map.getCenter(); 255 | 256 | // gl.setView([center.lat, center.lng], this._map.getZoom() - 1, 0); 257 | // calling setView directly causes sync issues because it uses requestAnimFrame 258 | 259 | const tr = gl.transform; 260 | tr.center = maplibregl.LngLat.convert([center.lng, center.lat]); 261 | tr.zoom = this._map.getZoom() - 1; 262 | }, 263 | 264 | // update the map constantly during a pinch zoom 265 | _pinchZoom: function (e) { 266 | this._glMap.jumpTo({ 267 | zoom: this._map.getZoom() - 1, 268 | center: this._map.getCenter() 269 | }); 270 | }, 271 | 272 | // borrowed from L.ImageOverlay 273 | // https://github.com/Leaflet/Leaflet/blob/master/src/layer/ImageOverlay.js#L139-L144 274 | _animateZoom: function (e) { 275 | const scale = this._map.getZoomScale(e.zoom); 276 | const padding = this._map 277 | .getSize() 278 | .multiplyBy(this.options.padding * scale); 279 | const viewHalf = this.getSize()._divideBy(2); 280 | // corrections for padding (scaled), adapted from 281 | // https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L1490-L1508 282 | const topLeft = this._map 283 | .project(e.center, e.zoom) 284 | ._subtract(viewHalf) 285 | ._add(this._map._getMapPanePos().add(padding)) 286 | ._round(); 287 | const offset = this._map 288 | .project(this._map.getBounds().getNorthWest(), e.zoom) 289 | ._subtract(topLeft); 290 | 291 | DomUtil.setTransform( 292 | this._glMap._actualCanvas, 293 | offset.subtract(this._offset), 294 | scale 295 | ); 296 | }, 297 | 298 | _zoomStart: function (e) { 299 | this._zooming = true; 300 | }, 301 | 302 | _zoomEnd: function () { 303 | const scale = this._map.getZoomScale(this._map.getZoom()); 304 | 305 | DomUtil.setTransform(this._glMap._actualCanvas, null, scale); 306 | 307 | this._zooming = false; 308 | 309 | this._update(); 310 | }, 311 | 312 | _transitionEnd: function (e) { 313 | Util.requestAnimFrame(function () { 314 | const zoom = this._map.getZoom(); 315 | const center = this._map.getCenter(); 316 | const offset = this._map.latLngToContainerPoint( 317 | this._map.getBounds().getNorthWest() 318 | ); 319 | 320 | // reset the scale and offset 321 | DomUtil.setTransform(this._glMap._actualCanvas, offset, 1); 322 | 323 | // enable panning once the gl map is ready again 324 | this._glMap.once( 325 | 'moveend', 326 | Util.bind(function () { 327 | this._zoomEnd(); 328 | }, this) 329 | ); 330 | 331 | // update the map position 332 | this._glMap.jumpTo({ 333 | center: center, 334 | zoom: zoom - 1 335 | }); 336 | }, this); 337 | } 338 | }); 339 | 340 | export function maplibreGLJSLayer (options) { 341 | return new MaplibreGLJSLayer(options); 342 | } 343 | -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | import { latLng, latLngBounds } from 'leaflet'; 2 | import { request, Support, Util } from 'esri-leaflet'; 3 | 4 | /* 5 | utility to establish a URL for the basemap styles API 6 | used primarily by VectorBasemapLayer.js 7 | */ 8 | export function getBasemapStyleUrl (style, apikey) { 9 | if (style.includes('/')) { 10 | throw new Error( 11 | style + ' is a v2 style enumeration. Set version:2 to request this style' 12 | ); 13 | } 14 | 15 | let url = 16 | 'https://basemaps-api.arcgis.com/arcgis/rest/services/styles/' + 17 | style + 18 | '?type=style'; 19 | if (apikey) { 20 | url = url + '&token=' + apikey; 21 | } 22 | return url; 23 | } 24 | 25 | /** 26 | * Utility to establish a URL for the basemap styles API v2 27 | * 28 | * @param {string} style 29 | * @param {string} token 30 | * @param {Object} [options] Optional list of options: language, worldview, or places. 31 | * @returns {string} the URL 32 | */ 33 | export function getBasemapStyleV2Url (style, token, options) { 34 | if (style.startsWith('osm/')) { 35 | console.log( 36 | "L.esri.Vector.vectorBasemapLayer: All 'osm/*' styles are retired are no longer receiving updates and were last updated in 2024. Please use 'open/*' styles instead." 37 | ); 38 | } 39 | 40 | if (style.includes(':')) { 41 | throw new Error( 42 | style + ' is a v1 style enumeration. Set version:1 to request this style' 43 | ); 44 | } 45 | 46 | let url = 47 | options.baseUrl || 48 | 'https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/'; 49 | 50 | if ( 51 | !( 52 | style.startsWith('open/') || 53 | style.startsWith('osm/') || 54 | style.startsWith('arcgis/') 55 | ) && 56 | style.length === 32 57 | ) { 58 | // style is an itemID 59 | url = url + 'items/' + style; 60 | 61 | if (options.language) { 62 | throw new Error( 63 | "The 'language' parameter is not supported for custom basemap styles" 64 | ); 65 | } 66 | } else { 67 | url = url + style; 68 | } 69 | 70 | if (!token) throw new Error('A token is required to access basemap styles.'); 71 | 72 | url = url + '?token=' + token; 73 | if (options.language) { 74 | url = url + '&language=' + options.language; 75 | } 76 | if (options.worldview) { 77 | url = url + '&worldview=' + options.worldview; 78 | } 79 | if (options.places) { 80 | url = url + '&places=' + options.places; 81 | } 82 | return url; 83 | } 84 | /* 85 | utilities to communicate with custom user styles via an ITEM ID or SERVICE URL 86 | used primarily by VectorTileLayer.js 87 | */ 88 | export function loadStyle (idOrUrl, options, callback) { 89 | const httpRegex = /^https?:\/\//; 90 | const serviceRegex = /\/VectorTileServer\/?$/; 91 | 92 | if (httpRegex.test(idOrUrl) && serviceRegex.test(idOrUrl)) { 93 | const serviceUrl = idOrUrl; 94 | loadStyleFromService(serviceUrl, options, callback); 95 | } else { 96 | const itemId = idOrUrl; 97 | loadStyleFromItem(itemId, options, callback); 98 | } 99 | } 100 | 101 | export function loadService (serviceUrl, options, callback) { 102 | const params = options.token ? { token: options.token } : {}; 103 | request(serviceUrl, params, callback); 104 | } 105 | 106 | function loadItem (itemId, options, callback) { 107 | const params = options.token ? { token: options.token } : {}; 108 | const url = options.portalUrl + '/sharing/rest/content/items/' + itemId; 109 | request(url, params, callback); 110 | } 111 | 112 | function loadStyleFromItem (itemId, options, callback) { 113 | const itemStyleUrl = toCdnUrl( 114 | options.portalUrl + 115 | '/sharing/rest/content/items/' + 116 | itemId + 117 | '/resources/styles/root.json' 118 | ); 119 | 120 | loadStyleFromUrl(itemStyleUrl, options, function (error, style) { 121 | if (error) { 122 | loadItem(itemId, options, function (error, item) { 123 | if (error) { 124 | callback(error); 125 | return; 126 | } 127 | loadStyleFromService(item.url, options, callback); 128 | }); 129 | } else { 130 | loadItem(itemId, options, function (error, item) { 131 | if (error) { 132 | callback(error); 133 | return; 134 | } 135 | loadService(item.url, options, function (error, service) { 136 | callback(error, style, itemStyleUrl, service, item.url); 137 | }); 138 | }); 139 | } 140 | }); 141 | } 142 | 143 | function loadStyleFromService (serviceUrl, options, callback) { 144 | loadService(serviceUrl, options, function (error, service) { 145 | if (error) { 146 | callback(error); 147 | return; 148 | } 149 | 150 | let sanitizedServiceUrl = serviceUrl; 151 | // a trailing "/" may create invalid paths 152 | if (serviceUrl.charAt(serviceUrl.length - 1) === '/') { 153 | sanitizedServiceUrl = serviceUrl.slice(0, serviceUrl.length - 1); 154 | } 155 | 156 | let defaultStylesUrl; 157 | // inadvertently inserting more than 1 adjacent "/" may create invalid paths 158 | if (service.defaultStyles.charAt(0) === '/') { 159 | defaultStylesUrl = 160 | sanitizedServiceUrl + service.defaultStyles + '/root.json'; 161 | } else { 162 | defaultStylesUrl = 163 | sanitizedServiceUrl + '/' + service.defaultStyles + '/root.json'; 164 | } 165 | 166 | loadStyleFromUrl(defaultStylesUrl, options, function (error, style) { 167 | if (error) { 168 | callback(error); 169 | return; 170 | } 171 | callback(null, style, defaultStylesUrl, service, serviceUrl); 172 | }); 173 | }); 174 | } 175 | 176 | function loadStyleFromUrl (styleUrl, options, callback) { 177 | const params = options.token ? { token: options.token } : {}; 178 | request(styleUrl, params, callback); 179 | } 180 | 181 | function isSameTLD (url1, url2) { 182 | return new URL(url1).hostname === new URL(url2).hostname; 183 | } 184 | 185 | /** 186 | * Converts an ArcGIS Online URL to a CDN URL to reduce latency and server load. This will not 187 | * convert a ArcGIS Enterprise URL since they will be hosting their own resources. 188 | * 189 | * Borrowed from the JS API. 190 | */ 191 | function toCdnUrl (url) { 192 | if (!url) { 193 | return url || null; 194 | } 195 | 196 | let outUrl = url; 197 | 198 | if (outUrl) { 199 | outUrl = normalizeArcGISOnlineOrgDomain(outUrl); 200 | outUrl = outUrl.replace( 201 | /^https?:\/\/www\.arcgis\.com/, 202 | 'https://cdn.arcgis.com' 203 | ); 204 | outUrl = outUrl.replace( 205 | /^https?:\/\/devext\.arcgis\.com/, 206 | 'https://cdndev.arcgis.com' 207 | ); 208 | outUrl = outUrl.replace( 209 | /^https?:\/\/qaext\.arcgis\.com/, 210 | 'https://cdnqa.arcgis.com' 211 | ); 212 | } 213 | 214 | return outUrl; 215 | } 216 | 217 | /** 218 | * Replaces the AGOL org domains with non-org domains. 219 | * 220 | * Borrowed from the JS API. 221 | */ 222 | function normalizeArcGISOnlineOrgDomain (url) { 223 | const prdOrg = /^https?:\/\/(?:cdn|[a-z\d-]+\.maps)\.arcgis\.com/i; // https://cdn.arcgis.com or https://x.maps.arcgis.com 224 | const devextOrg = 225 | /^https?:\/\/(?:cdndev|[a-z\d-]+\.mapsdevext)\.arcgis\.com/i; // https://cdndev.arcgis.com or https://x.mapsdevext.arcgis.com 226 | const qaOrg = /^https?:\/\/(?:cdnqa|[a-z\d-]+\.mapsqa)\.arcgis\.com/i; // https://cdnqa.arcgis.com or https://x.mapsqa.arcgis.com 227 | 228 | // replace AGOL org domains with non-org domains 229 | if (prdOrg.test(url)) { 230 | url = url.replace(prdOrg, 'https://www.arcgis.com'); 231 | } else if (devextOrg.test(url)) { 232 | url = url.replace(devextOrg, 'https://devext.arcgis.com'); 233 | } else if (qaOrg.test(url)) { 234 | url = url.replace(qaOrg, 'https://qaext.arcgis.com'); 235 | } 236 | 237 | return url; 238 | } 239 | 240 | export function formatStyle (style, styleUrl, metadata, token) { 241 | // transforms style object in place and also returns it 242 | 243 | // modify each source in style.sources 244 | const sourcesKeys = Object.keys(style.sources); 245 | 246 | for (let sourceIndex = 0; sourceIndex < sourcesKeys.length; sourceIndex++) { 247 | const source = style.sources[sourcesKeys[sourceIndex]]; 248 | 249 | // if a relative path is referenced, the default style can be found in a standard location 250 | if (source.url.indexOf('http') === -1) { 251 | source.url = styleUrl.replace('/resources/styles/root.json', ''); 252 | } 253 | 254 | // a trailing "/" may create invalid paths 255 | if (source.url.charAt(source.url.length - 1) === '/') { 256 | source.url = source.url.slice(0, source.url.length - 1); 257 | } 258 | 259 | // add tiles property if missing 260 | if (!source.tiles) { 261 | // right now ArcGIS Pro published vector services have a slightly different signature 262 | // the '/' is needed in the URL string concatenation below for source.tiles 263 | if (metadata.tiles && metadata.tiles[0].charAt(0) !== '/') { 264 | metadata.tiles[0] = '/' + metadata.tiles[0]; 265 | } 266 | 267 | source.tiles = [source.url + metadata.tiles[0]]; 268 | } 269 | 270 | // some VectorTileServer endpoints may default to returning f=html, 271 | // specify f=json to account for that behavior 272 | source.url += '?f=json'; 273 | 274 | // add the token to the source url and tiles properties as a query param 275 | source.url += token ? '&token=' + token : ''; 276 | source.tiles[0] += token ? '?token=' + token : ''; 277 | // add minzoom and maxzoom to each source based on the service metadata 278 | // prefer minLOD/maxLOD if it exists since that is the level that tiles are cooked too 279 | // MapLibre will overzoom for LODs that are not cooked 280 | source.minzoom = metadata.minLOD || metadata.tileInfo.lods[0].level; 281 | source.maxzoom = 282 | metadata.maxLOD || 283 | metadata.tileInfo.lods[metadata.tileInfo.lods.length - 1].level; 284 | } 285 | 286 | // add the attribution and copyrightText properties to the last source in style.sources based on the service metadata 287 | const lastSource = style.sources[sourcesKeys[sourcesKeys.length - 1]]; 288 | lastSource.attribution = metadata.copyrightText || ''; 289 | lastSource.copyrightText = metadata.copyrightText || ''; 290 | 291 | // if any layer in style.layers has a layout.text-font property (it will be any array of strings) remove all items in the array after the first 292 | for (let layerIndex = 0; layerIndex < style.layers.length; layerIndex++) { 293 | const layer = style.layers[layerIndex]; 294 | if ( 295 | layer.layout && 296 | layer.layout['text-font'] && 297 | layer.layout['text-font'].length > 1 298 | ) { 299 | layer.layout['text-font'] = [layer.layout['text-font'][0]]; 300 | } 301 | } 302 | 303 | if (style.sprite && style.sprite.indexOf('http') === -1) { 304 | // resolve absolute URL for style.sprite 305 | style.sprite = styleUrl.replace( 306 | 'styles/root.json', 307 | style.sprite.replace('../', '') 308 | ); 309 | } 310 | 311 | // Convert the style.glyphs and style.sprite URLs to CDN URLs if possable 312 | if (style.glyphs) { 313 | style.glyphs = toCdnUrl(style.glyphs); 314 | } 315 | 316 | if (style.sprite) { 317 | style.sprite = toCdnUrl(style.sprite); 318 | } 319 | 320 | // a trailing "/" may create invalid paths 321 | if (style.sprite && token) { 322 | // add the token to the style.sprite property as a query param, only if same domain (for token security) 323 | if (isSameTLD(styleUrl, style.sprite)) { 324 | style.sprite += '?token=' + token; 325 | } else { 326 | console.warn( 327 | 'Passing a token but sprite URL is not on same base URL, so you must pass the token manually.' 328 | ); 329 | } 330 | } 331 | 332 | if (style.glyphs && style.glyphs.indexOf('http') === -1) { 333 | // resolve absolute URL for style.glyphs 334 | style.glyphs = styleUrl.replace( 335 | 'styles/root.json', 336 | style.glyphs.replace('../', '') 337 | ); 338 | } 339 | 340 | if (style.glyphs && token) { 341 | // add the token to the style.glyphs property as a query param 342 | if (isSameTLD(styleUrl, style.glyphs)) { 343 | style.glyphs += '?token=' + token; 344 | } else { 345 | console.warn( 346 | 'Passing a token but glyph URL is not on same base URL, so you must pass the token manually.' 347 | ); 348 | } 349 | } 350 | 351 | return style; 352 | } 353 | 354 | /* 355 | utility to assist with dynamic attribution data 356 | used primarily by VectorBasemapLayer.js 357 | */ 358 | export function getAttributionData (url, map) { 359 | if (Support.cors) { 360 | request(url, {}, function (error, attributions) { 361 | if (error) { 362 | return; 363 | } 364 | map._esriAttributions = map._esriAttributions || []; 365 | for (let c = 0; c < attributions.contributors.length; c++) { 366 | const contributor = attributions.contributors[c]; 367 | 368 | for (let i = 0; i < contributor.coverageAreas.length; i++) { 369 | const coverageArea = contributor.coverageAreas[i]; 370 | const southWest = latLng(coverageArea.bbox[0], coverageArea.bbox[1]); 371 | const northEast = latLng(coverageArea.bbox[2], coverageArea.bbox[3]); 372 | map._esriAttributions.push({ 373 | attribution: contributor.attribution, 374 | score: coverageArea.score, 375 | bounds: latLngBounds(southWest, northEast), 376 | minZoom: coverageArea.zoomMin, 377 | maxZoom: coverageArea.zoomMax 378 | }); 379 | } 380 | } 381 | 382 | map._esriAttributions.sort(function (a, b) { 383 | return b.score - a.score; 384 | }); 385 | 386 | // pass the same argument as the map's 'moveend' event 387 | const obj = { target: map }; 388 | Util._updateMapAttribution(obj); 389 | }); 390 | } 391 | } 392 | 393 | /* 394 | utility to check if a service's tileInfo spatial reference is in Web Mercator 395 | used primarily by VectorTileLayer.js 396 | */ 397 | const WEB_MERCATOR_WKIDS = [3857, 102100, 102113]; 398 | 399 | export function isWebMercator (wkid) { 400 | return WEB_MERCATOR_WKIDS.indexOf(wkid) >= 0; 401 | } 402 | 403 | export const EsriUtil = { 404 | formatStyle: formatStyle 405 | }; 406 | 407 | export default EsriUtil; 408 | -------------------------------------------------------------------------------- /src/VectorBasemapLayer.js: -------------------------------------------------------------------------------- 1 | import { Util } from 'esri-leaflet'; 2 | import { 3 | getBasemapStyleUrl, 4 | getAttributionData, 5 | getBasemapStyleV2Url 6 | } from './Util'; 7 | import { VectorTileLayer } from './VectorTileLayer'; 8 | 9 | const POWERED_BY_ESRI_ATTRIBUTION_STRING = 10 | 'Powered by Esri'; 11 | 12 | export const VectorBasemapLayer = VectorTileLayer.extend({ 13 | /** 14 | * Populates "this.options" to be used in the rest of the module. 15 | * 16 | * @param {string} key 17 | * @param {object} options optional 18 | */ 19 | initialize: function (key, options) { 20 | // Default to the v1 service endpoint 21 | if (!options.version) { 22 | if (key.includes('/')) options.version = 2; 23 | else options.version = 1; 24 | } 25 | if (!key) { 26 | // Default style enum if none provided 27 | key = options.version === 1 ? 'ArcGIS:Streets' : 'arcgis/streets'; 28 | } 29 | // If no API Key or token is provided (support outdated casing apiKey of apikey) 30 | if (!(options.apikey || options.apiKey || options.token)) { 31 | throw new Error( 32 | 'An API Key or token is required for vectorBasemapLayer.' 33 | ); 34 | } 35 | // Validate v2 service params 36 | if (options.version !== 2) { 37 | if (options.language) { 38 | throw new Error( 39 | 'The language parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 40 | ); 41 | } 42 | if (options.worldview) { 43 | throw new Error( 44 | 'The worldview parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 45 | ); 46 | } 47 | if (options.places) { 48 | throw new Error( 49 | 'The places parameter is only supported by the basemap styles service v2. Provide a v2 style enumeration to use this option.' 50 | ); 51 | } 52 | } 53 | // Determine layer order 54 | if (!options.pane) { 55 | if (key.includes(':Label') || key.includes('/label')) { 56 | options.pane = 'esri-labels'; 57 | } else if (key.includes(':Detail') || key.includes('/detail')) { 58 | options.pane = 'esri-detail'; 59 | } else { 60 | // Create layer in the tilePane by default 61 | options.pane = 'tilePane'; 62 | } 63 | } 64 | 65 | // Options has been configured, continue on to create the layer: 66 | VectorTileLayer.prototype.initialize.call(this, key, options); 67 | }, 68 | 69 | /** 70 | * Creates the maplibreGLJSLayer using "this.options" 71 | */ 72 | _createLayer: function () { 73 | let styleUrl; 74 | if (this.options.version && this.options.version === 2) { 75 | styleUrl = getBasemapStyleV2Url(this.options.key, this.options.apikey, { 76 | language: this.options.language, 77 | worldview: this.options.worldview, 78 | places: this.options.places, 79 | baseUrl: this.options.baseUrl 80 | }); 81 | } else { 82 | styleUrl = getBasemapStyleUrl(this.options.key, this.options.apikey); 83 | } 84 | // show error warning on successful response for previous version(1) 85 | if (this.options.version && this.options.version === 1) { 86 | fetch(styleUrl) 87 | .then((response) => { 88 | return response.json(); 89 | }) 90 | .then((styleData) => { 91 | if (styleData.error) { 92 | console.warn('Error:', styleData.error.message); 93 | } 94 | }) 95 | .catch((error) => { 96 | console.warn('Error:', error.message); 97 | }); 98 | } 99 | this._createMaplibreLayer(styleUrl); 100 | }, 101 | 102 | _setupAttribution: function () { 103 | if (this.options.key.length === 32) { 104 | // this is an itemId 105 | const sources = 106 | this._maplibreGL.getMaplibreMap().style.stylesheet.sources; 107 | const allAttributions = []; 108 | Object.keys(sources).forEach(function (key) { 109 | allAttributions.push(sources[key].attribution); 110 | if ( 111 | sources[key].copyrightText && 112 | sources[key].copyrightText && 113 | sources[key].copyrightText !== '' && 114 | sources[key].attribution !== sources[key].copyrightText 115 | ) { 116 | allAttributions.push(sources[key].copyrightText); 117 | } 118 | }); 119 | 120 | // In the case of an enum, since the attribution is dynamic, Esri Leaflet 121 | // will add the "Powered by Esri" string. But in this case we are not 122 | // dynamic so we must add it ourselves. 123 | this._map.attributionControl.addAttribution( 124 | `${POWERED_BY_ESRI_ATTRIBUTION_STRING} | ${allAttributions.join( 125 | ', ' 126 | )}` 127 | ); 128 | } else { 129 | // setup dynamic attribution 130 | Util.setEsriAttribution(this._map); 131 | 132 | // this is an enum 133 | if (!this.options.attributionUrls) { 134 | this.options.attributionUrls = this._getAttributionUrls( 135 | this.options.key 136 | ); 137 | } 138 | 139 | if (this._map && this.options.attributionUrls) { 140 | if (this._map.attributionControl) { 141 | for ( 142 | let index = 0; 143 | index < this.options.attributionUrls.length; 144 | index++ 145 | ) { 146 | const attributionUrl = this.options.attributionUrls[index]; 147 | getAttributionData(attributionUrl, this._map); 148 | } 149 | 150 | this._map.attributionControl.addAttribution( 151 | '' 152 | ); 153 | } 154 | Util._updateMapAttribution({ target: this._map }); 155 | } 156 | } 157 | }, 158 | 159 | /** 160 | * Given a key, return the attribution url(s). 161 | * @param {string} key 162 | */ 163 | _getAttributionUrls: function (key) { 164 | if (key.indexOf('OSM:') === 0 || key.indexOf('osm/') === 0) { 165 | return ['https://static.arcgis.com/attribution/Vector/OpenStreetMap_v2']; 166 | } else if ( 167 | key.indexOf('ArcGIS:Imagery') === 0 || 168 | key.indexOf('arcgis/imagery') === 0 169 | ) { 170 | return [ 171 | 'https://static.arcgis.com/attribution/World_Imagery', 172 | 'https://static.arcgis.com/attribution/Vector/World_Basemap_v2' 173 | ]; 174 | } 175 | 176 | // default: 177 | return ['https://static.arcgis.com/attribution/Vector/World_Basemap_v2']; 178 | }, 179 | 180 | _initPane: function () { 181 | if (!this._map.getPane(this.options.pane)) { 182 | const pane = this._map.createPane(this.options.pane); 183 | pane.style.pointerEvents = 'none'; 184 | 185 | let zIndex = 500; 186 | if (this.options.pane === 'esri-detail') { 187 | zIndex = 250; 188 | } else if (this.options.pane === 'esri-labels') { 189 | zIndex = 300; 190 | } 191 | pane.style.zIndex = zIndex; 192 | } 193 | }, 194 | 195 | onRemove: function (map) { 196 | map.off('moveend', Util._updateMapAttribution); 197 | map.removeLayer(this._maplibreGL); 198 | 199 | if (map.attributionControl) { 200 | if (Util.removeEsriAttribution) Util.removeEsriAttribution(map); 201 | 202 | const element = document.getElementsByClassName( 203 | 'esri-dynamic-attribution' 204 | ); 205 | 206 | if (element && element.length > 0) { 207 | const vectorAttribution = element[0].outerHTML; 208 | // call removeAttribution twice here 209 | // this is needed due to the 2 different ways that addAttribution is called inside _setupAttribution. 210 | // leaflet attributionControl.removeAttribution method ignore a call when the attribution sent is not present there 211 | map.attributionControl.removeAttribution(vectorAttribution); 212 | map.attributionControl.removeAttribution( 213 | '' 214 | ); 215 | } 216 | } 217 | }, 218 | 219 | _asyncAdd: function () { 220 | const map = this._map; 221 | this._initPane(); 222 | map.on('moveend', Util._updateMapAttribution); 223 | this._maplibreGL.addTo(map, this); 224 | } 225 | }); 226 | 227 | export function vectorBasemapLayer (key, options) { 228 | return new VectorBasemapLayer(key, options); 229 | } 230 | 231 | export default vectorBasemapLayer; 232 | -------------------------------------------------------------------------------- /src/VectorTileLayer.js: -------------------------------------------------------------------------------- 1 | import { Layer, setOptions } from 'leaflet'; 2 | import { loadStyle, formatStyle, isWebMercator } from './Util'; 3 | import { maplibreGLJSLayer } from './MaplibreGLLayer'; 4 | 5 | export const VectorTileLayer = Layer.extend({ 6 | options: { 7 | // if portalUrl is not provided, default to ArcGIS Online 8 | portalUrl: 'https://www.arcgis.com', 9 | // for performance optimization default to `false` 10 | // set to `true` to resolve printing issues in Firefox 11 | preserveDrawingBuffer: false 12 | }, 13 | 14 | /** 15 | * Populates "this.options" to be used in the rest of the module and creates the layer instance. 16 | * 17 | * @param {string} key an ITEM ID or SERVICE URL 18 | * @param {object} options optional 19 | */ 20 | initialize: function (key, options) { 21 | if (options) { 22 | setOptions(this, options); 23 | } 24 | 25 | // support outdated casing apiKey of apikey 26 | if (this.options.apiKey) { 27 | this.options.apikey = this.options.apiKey; 28 | } 29 | 30 | // if apiKey is passed in, propagate to token 31 | // if token is passed in, propagate to apiKey 32 | if (this.options.apikey) { 33 | this.options.token = this.options.apikey; 34 | } else if (this.options.token) { 35 | this.options.apikey = this.options.token; 36 | } 37 | 38 | // if no key passed in 39 | if (!key) { 40 | throw new Error( 41 | 'An ITEM ID or SERVICE URL is required for vectorTileLayer.' 42 | ); 43 | } 44 | 45 | // set key onto "this.options" for use elsewhere in the module. 46 | if (key) { 47 | this.options.key = key; 48 | } 49 | 50 | // this.options has been set, continue on to create the layer: 51 | this._createLayer(); 52 | }, 53 | 54 | /** 55 | * Creates the maplibreGLJSLayer given using "this.options" 56 | */ 57 | _createLayer: function () { 58 | loadStyle( 59 | this.options.key, 60 | this.options, 61 | function (error, style, styleUrl, service) { 62 | if (error) { 63 | this.fire('load-error', { 64 | value: error 65 | }); 66 | return; 67 | } 68 | 69 | if (!isWebMercator(service.tileInfo.spatialReference.wkid)) { 70 | console.warn( 71 | 'This layer is not guaranteed to display properly because its service does not use the Web Mercator projection. The "tileInfo.spatialReference" property is:', 72 | service.tileInfo.spatialReference, 73 | '\nMore information is available at https://github.com/maplibre/maplibre-gl-js/issues/168 and https://github.com/Esri/esri-leaflet-vector/issues/94.' 74 | ); 75 | } 76 | 77 | // once style object is loaded it must be transformed to be compliant with maplibreGLJSLayer 78 | style = formatStyle(style, styleUrl, service, this.options.token); 79 | 80 | this._createMaplibreLayer(style); 81 | }.bind(this) 82 | ); 83 | }, 84 | 85 | _setupAttribution: function () { 86 | // if a custom attribution was not provided in the options, 87 | // then attempt to rely on the attribution of the last source in the style object 88 | // and add it to the map's attribution control 89 | // (otherwise it would have already been added by leaflet to the attribution control) 90 | if (!this.getAttribution()) { 91 | const sources = 92 | this._maplibreGL.getMaplibreMap().style.stylesheet.sources; 93 | const sourcesKeys = Object.keys(sources); 94 | this.options.attribution = 95 | sources[sourcesKeys[sourcesKeys.length - 1]].attribution; 96 | if (this._map && this._map.attributionControl) { 97 | // NOTE: if attribution is an empty string (or otherwise falsy) at this point it would not appear in the attribution control 98 | this._map.attributionControl.addAttribution(this.getAttribution()); 99 | } 100 | } 101 | }, 102 | 103 | _createMaplibreLayer: function (style) { 104 | this._maplibreGL = maplibreGLJSLayer({ 105 | style: style, 106 | pane: this.options.pane, 107 | opacity: this.options.opacity, 108 | preserveDrawingBuffer: this.options.preserveDrawingBuffer 109 | }); 110 | 111 | this._ready = true; 112 | this.fire('ready', {}, true); 113 | 114 | this._maplibreGL.on( 115 | 'styleLoaded', 116 | function () { 117 | this._setupAttribution(); 118 | // additionally modify the style object with the user's optional style override function 119 | if (this.options.style && typeof this.options.style === 'function') { 120 | this._maplibreGL._glMap.setStyle( 121 | this.options.style(this._maplibreGL._glMap.getStyle()) 122 | ); 123 | } 124 | }.bind(this) 125 | ); 126 | }, 127 | 128 | onAdd: function (map) { 129 | this._map = map; 130 | 131 | if (this._ready) { 132 | this._asyncAdd(); 133 | } else { 134 | this.once( 135 | 'ready', 136 | function () { 137 | this._asyncAdd(); 138 | }, 139 | this 140 | ); 141 | } 142 | }, 143 | 144 | onRemove: function (map) { 145 | map.removeLayer(this._maplibreGL); 146 | }, 147 | 148 | _asyncAdd: function () { 149 | const map = this._map; 150 | this._maplibreGL.addTo(map, this); 151 | } 152 | }); 153 | 154 | export function vectorTileLayer (key, options) { 155 | return new VectorTileLayer(key, options); 156 | } 157 | 158 | export default vectorTileLayer; 159 | --------------------------------------------------------------------------------