├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── pages.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── PUBLISHING.md ├── README.md ├── eslint.config.js ├── examples ├── _template.html ├── apply-layergroup.js ├── data │ ├── circles-style.json │ ├── geojson-inline.json │ ├── geojson-wfs.json │ ├── geojson-wms.json │ ├── geojson.json │ ├── polygons-style.json │ ├── protomaps-dark-style.json │ ├── sprites-bw.json │ ├── sprites-bw.png │ ├── sprites-bw@2x.json │ ├── sprites-bw@2x.png │ ├── sprites.json │ ├── sprites.png │ ├── sprites@2x.json │ ├── sprites@2x.png │ ├── states.geojson │ ├── states.json │ ├── tilejson.json │ └── wms.json ├── esri-4326.js ├── esri-transformrequest.js ├── geojson-featurestate.js ├── geojson-inline.js ├── geojson-layer.js ├── geojson-wfs.js ├── geojson.js ├── index.html ├── mapbox.js ├── maptiler-hillshading.js ├── openmaptiles-layer.js ├── openmaptiles.js ├── pmtiles.js ├── sdf-sprites.js ├── stylefunction.js ├── terrarium-hillshading.js ├── tilejson-vectortile.js ├── tilejson.js ├── versatiles.js └── wms.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── MapboxVectorLayer.js ├── apply.js ├── index.js ├── mapbox.js ├── shaders.js ├── stylefunction.js ├── text.js └── util.js ├── test ├── MapboxVectorLayer.test.js ├── apply.test.js ├── applyStyle.test.js ├── finalizeLayer.test.js ├── fixtures │ ├── background-none.json │ ├── background.json │ ├── geography-class.json │ ├── geojson-inline.json │ ├── geojson-wfs.json │ ├── geojson.json │ ├── hospital.png │ ├── hot-osm │ │ ├── hot-osm.json │ │ ├── osm.json │ │ ├── osm_tegola_spritesheet.json │ │ ├── osm_tegola_spritesheet.png │ │ ├── osm_tegola_spritesheet@2x.json │ │ └── osm_tegola_spritesheet@2x.png │ ├── osm-liberty │ │ ├── osm-liberty.json │ │ ├── osm-liberty.png │ │ ├── osm-liberty@2x.json │ │ ├── osm-liberty@2x.png │ │ ├── style.json │ │ └── tiles.json │ ├── raster-dem.json │ ├── response.json │ ├── sprites.json │ ├── sprites.png │ ├── sprites@2x.json │ ├── sprites@2x.png │ ├── states.geojson │ ├── states.json │ ├── style-empty-sprite.json │ ├── style-invalid-sprite-url.json │ ├── style-invalid-version.json │ ├── style-missing-sprite.json │ ├── style-with-multiple-background-layers.json │ ├── tilejson-mapboxvector.json │ ├── tilejson.json │ ├── tilejson.raster.json │ └── wms.json ├── index_test.cjs ├── karma.conf.cjs ├── mapbox.test.js ├── stylefunction-utils.test.js ├── stylefunction.test.js ├── test.html ├── text.test.js └── util.test.js ├── tsconfig-build.json ├── tsconfig-typecheck.json ├── tsconfig.json ├── typedoc.json └── webpack.config.examples.cjs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_size = 2 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Prepare 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '22' 32 | cache: 'npm' 33 | - run: npm ci 34 | - name: Upload static files as artifact 35 | id: deployment 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: _site/ 39 | deploy: 40 | environment: 41 | name: github-pages 42 | url: ${{ steps.deployment.outputs.page_url }} 43 | runs-on: ubuntu-latest 44 | needs: build 45 | steps: 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '20.x' 21 | cache: 'npm' 22 | - run: npm ci 23 | - run: npm test 24 | - run: npm run build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | _site 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci/ 2 | .git/ 3 | .github/ 4 | coverage/ 5 | _site/ 6 | examples/ 7 | test/ 8 | dist/examples/ 9 | .editorconfig 10 | .eslintrc 11 | webpack.*.cjs 12 | rollup.config.js 13 | tsconfig*.json 14 | typedoc.json 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-present ol-mapbox-style contributors 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | To create and publish a release, perform the following steps: 2 | 3 | ### Create a release branch 4 | 5 | In order to make these release-related changes, create a branch in your repository clone. 6 | Note that all the examples below use 2.11.0 as the release version, you'll want to use the appropriate version numbers for the release you're working toward. 7 | 8 | git checkout -b release-v2.11.0 origin/main 9 | 10 | ### Bump the version in package.json 11 | 12 | We use [semantic versioning](https://semver.org). Set the correct `"version"` in package.json. Run `npm install` so `package-lock.json` can pick up the changes. 13 | 14 | Edit `CHANGELOG.md`: Add the version you are about to release just below the `## Next version` heading. Review the changes since the last release and document changes as appropriate. 15 | 16 | Commit the changes. 17 | 18 | git add package.json package-lock.json CHANGELOG.md 19 | git commit -m "Changes for 2.11.0" 20 | 21 | 22 | ### Merge the release branch 23 | 24 | Create a pull request 25 | 26 | git push origin release-v2.10.0 27 | 28 | and merge the release branch. This allows for any final review of upgrade notes or other parts of the changelog. 29 | 30 | ### Publish to npm 31 | 32 | npm publish 33 | 34 | ### Commit release artifacts 35 | 36 | git add -f dist/* 37 | git commit -m "Add dist for v2.11.0" 38 | 39 | ### Create and push a tag 40 | 41 | git tag -a v2.11.0 -m "2.11.0" 42 | git push --tags origin 43 | 44 | ### Edit the release notes 45 | 46 | The previous step creates a release on GitHub. Copy the changelog for the relese from `CHANGELOG.md` to the "Describe this release" field for the release notes on https://github.com/openlayers/ol-mapbox-style/releases. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Create [OpenLayers](https://openlayers.org/) maps from [Mapbox Style](https://docs.mapbox.com/mapbox-gl-js/style-spec/) or [MapLibre Style](https://maplibre.org/maplibre-style-spec/) objects. 2 | 3 | ## Getting started 4 | 5 | Get an impression of what this library does by exploring the [live examples](https://openlayers.org/ol-mapbox-style/examples/). 6 | 7 | ### Installation 8 | 9 | To use the library in an application with an npm based dev environment, install it with 10 | 11 | npm install ol-mapbox-style 12 | 13 | When installed this way, just import from ol-mapbox-style, as shown in the usage examples below. To use a standalone build of ol-mapbox-style, include 'dist/olms.js' on your HTML page, and access the exported functions from the global `olms` object (e.g. `olms.apply()`, `olms.applyBackground()`). Note that the standalone build depends on the full build of OpenLayers. 14 | 15 | **ol-mapbox-style >=12.4 is required for [OpenLayers](https://npmjs.com/package/ol) >10.3.1**. 16 | 17 | **ol-mapbox-style >=9 requires [OpenLayers](https://npmjs.com/package/ol) version >=7 <11**. 18 | 19 | **ol-mapbox-style 8 requires [OpenLayers](https://npmjs.com/package/ol) version >=6.13 <7**. 20 | 21 | ### Usage 22 | 23 | **See the [project page](https://openlayers.org/ol-mapbox-style/) for the full documentation.** 24 | 25 | The code below creates an OpenLayers map from Mapbox's Bright v9 style, using a `https://` url: 26 | 27 | ```js 28 | import { apply } from 'ol-mapbox-style'; 29 | 30 | apply('map', 'https://api.mapbox.com/styles/v1/mapbox/bright-v9?access_token=YOUR_MAPBOX_TOKEN'); 31 | ``` 32 | 33 | To assign style and source to a layer only, use `applyStyle()`. `mapbox://` urls are also supported: 34 | 35 | ```js 36 | import {applyStyle} from 'ol-mapbox-style'; 37 | import VectorTileLayer from 'ol/layer/VectorTile.js' 38 | 39 | const layer = new VectorTileLayer({declutter: true}); 40 | applyStyle(layer, 'mapbox://styles/mapbox/bright-v9', {accessToken: 'YOUR_MAPBOX_TOKEN'}); 41 | ``` 42 | 43 | To apply the properties of the Mapbox/MapLibre Style's `background` layer to the map or a `VectorTile` layer, use the `applyBackground()` function. 44 | 45 | There is also a low-level API available. To create a style function for individual OpenLayers vector or vector tile layers, use the `stylefunction` module: 46 | 47 | ```js 48 | import {stylefunction} from 'ol-mapbox-style'; 49 | import VectorLayer from 'ol/layer/Vector.js'; 50 | import VectorSource from 'ol/source/Vector.js'; 51 | import GeoJSON from 'ol/format/GeoJSON.js'; 52 | 53 | const layer = new VectorLayer({ 54 | source: new VectorSource({ 55 | format: new GeoJSON(), 56 | url: 'data/states.geojson' 57 | }) 58 | }); 59 | 60 | fetch('data/states.json').then(function(response) { 61 | response.json().then(function(glStyle) { 62 | stylefunction(layer, glStyle, 'states'); 63 | }); 64 | }); 65 | ``` 66 | 67 | Note that this low-level API does not create a source for the layer, and extra work is required to set up sprite handling for styles that use icons. 68 | 69 | ## Compatibility notes 70 | 71 | ### Font handling 72 | 73 | `ol-mapbox-style` cannot use PBF/SDF glyphs for `text-font` layout property, as defined in the Mapbox/MapLibre Style specification. Instead, it relies on web fonts. A `ol:webfonts` metadata property can be set on the root of the Style object to specify a location for webfonts, e.g. 74 | 75 | ```js 76 | { 77 | "version": 8, 78 | "metadata": { 79 | "ol:webfonts": "https://my.server/fonts/{font-family}/{fontweight}{-fontstyle}.css" 80 | } 81 | // ... 82 | } 83 | ``` 84 | 85 | As an alternative, the `webfonts` option of the `apply()` or `applyStyle()` functions can be used. 86 | 87 | The following placeholders can be used in the template url: 88 | 89 | - `{font-family}`: CSS font family converted to lowercase, blanks replaced with -, e.g. noto-sans 90 | - `{Font+Family}`: CSS font family in original case, blanks replaced with +, e.g. Noto+Sans 91 | - `{fontweight}`: CSS font weight (numeric), e.g. 400, 700 92 | - `{fontstyle}`: CSS font style, e.g. normal, italic 93 | - `{-fontstyle}`: CSS font style other than normal, e.g. -italic or empty string for normal 94 | 95 | If no `metadata['ol:webfonts']` property is available on the Style object, [Fontsource Fonts](https://fontsource.org/) will be used. It is also possible for the application to load other fonts, using css. If a font is already available in the browser, `ol-mapbox-style` will not load it. 96 | 97 | Because of this difference, the [font stack](https://www.mapbox.com/help/manage-fontstacks/) is treated a little different than defined in the spec: style and weight are taken from the primary font (i.e. the first one in the font stack). Subsequent fonts in the font stack are only used if the primary font is not available/loaded, and they will be used with the style and weight of the primary font. 98 | 99 | ## Building the library 100 | 101 | npm run build 102 | 103 | The resulting distribution files will be in the `dist/` folder. To see the library in action, navigate to `dist/index.html`. 104 | 105 | To run test locally, run 106 | 107 | npm test 108 | 109 | For debugging tests in the browser, run 110 | 111 | npm run karma 112 | 113 | and open a browser on the host and port indicated in the console output (usually ) and click the 'DEBUG' button to go to the debug environment. 114 | 115 | [![Test Job](https://github.com/openlayers/ol-mapbox-style/actions/workflows/test.yml/badge.svg)](https://github.com/openlayers/ol-mapbox-style/actions/workflows/test.yml) 116 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import openlayers from 'eslint-config-openlayers'; 2 | 3 | /** 4 | * @type {Array} 5 | */ 6 | export default [ 7 | ...openlayers, 8 | { 9 | name: 'test-config', 10 | files: ['test/**/*'], 11 | languageOptions: { 12 | globals: { 13 | after: 'readonly', 14 | afterEach: 'readonly', 15 | afterLoadText: 'readonly', 16 | before: 'readonly', 17 | beforeEach: 'readonly', 18 | createMapDiv: 'readonly', 19 | defineCustomMapEl: 'readonly', 20 | expect: 'readonly', 21 | describe: 'readonly', 22 | disposeMap: 'readonly', 23 | it: 'readonly', 24 | render: 'readonly', 25 | where: 'readonly', 26 | }, 27 | }, 28 | }, 29 | { 30 | name: 'examples-config', 31 | files: ['examples/**/*'], 32 | rules: { 33 | 'import/no-unresolved': 'off', 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /examples/_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ol-mapbox-style example 8 | 18 | 19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /examples/apply-layergroup.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {Map, View} from 'ol'; 3 | import {Group as LayerGroup} from 'ol/layer.js'; 4 | import {apply} from 'ol-mapbox-style'; 5 | 6 | const layerGroup = new LayerGroup(); 7 | apply(layerGroup, 'data/geojson-wms.json'); 8 | 9 | new Map({ 10 | target: 'map', 11 | view: new View({ 12 | center: [-10203186.115192635, 4475744.563386], 13 | zoom: 4, 14 | }), 15 | layers: [layerGroup], 16 | }); 17 | -------------------------------------------------------------------------------- /examples/data/circles-style.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "zoom": 7, 4 | "sources": { 5 | "fwp_land": { 6 | "type": "geojson", 7 | "data": "https://sos-at-vie-1.exo.io/w3geo/waldgis/data/fwp_land.geojson" 8 | } 9 | }, 10 | "layers": [ 11 | { 12 | "id": "Mehr als 2 FWP des Landes", 13 | "type": "circle", 14 | "source": "fwp_land", 15 | "filter": [ 16 | "all", 17 | [ 18 | ">", 19 | [ 20 | "get", 21 | "#FWPLand" 22 | ], 23 | 2 24 | ], 25 | [ 26 | "<=", 27 | [ 28 | "get", 29 | "#FWPLand" 30 | ], 31 | 4 32 | ] 33 | ], 34 | "paint": { 35 | "circle-radius": [ 36 | "/", 37 | 17.857142857142854, 38 | 2 39 | ], 40 | "circle-color": "#df4f06", 41 | "circle-stroke-width": 1 42 | } 43 | }, 44 | { 45 | "id": "Zwei FWP des Landes", 46 | "type": "circle", 47 | "source": "fwp_land", 48 | "filter": [ 49 | "all", 50 | [ 51 | ">", 52 | [ 53 | "get", 54 | "#FWPLand" 55 | ], 56 | 1 57 | ], 58 | [ 59 | "<=", 60 | [ 61 | "get", 62 | "#FWPLand" 63 | ], 64 | 2 65 | ] 66 | ], 67 | "paint": { 68 | "circle-radius": [ 69 | "/", 70 | 14.285714285714285, 71 | 2 72 | ], 73 | "circle-color": "#fd9242", 74 | "circle-stroke-width": 1 75 | } 76 | }, 77 | { 78 | "id": "Ein FWP des Landes", 79 | "type": "circle", 80 | "source": "fwp_land", 81 | "filter": [ 82 | "all", 83 | [ 84 | ">=", 85 | [ 86 | "get", 87 | "#FWPLand" 88 | ], 89 | 0 90 | ], 91 | [ 92 | "<=", 93 | [ 94 | "get", 95 | "#FWPLand" 96 | ], 97 | 1 98 | ] 99 | ], 100 | "paint": { 101 | "circle-radius": [ 102 | "/", 103 | 10.714285714285714, 104 | 2 105 | ], 106 | "circle-color": "#fed1a7", 107 | "circle-stroke-width": 1 108 | } 109 | } 110 | ] 111 | } -------------------------------------------------------------------------------- /examples/data/geojson-wfs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "wfs", 4 | "center": [-79.882797, 43.513489], 5 | "zoom": 11, 6 | "glyphs": "{fontstack}/{range}", 7 | "sources": { 8 | "water_areas": { 9 | "type": "geojson", 10 | "data": "https://ahocevar.com/geoserver/wfs?service=WFS&version=1.1.0&request=GetFeature&typename=osm:water_areas&outputFormat=application/json&srsname=EPSG:4326&bbox={bbox-epsg-3857},EPSG:3857" 11 | }, 12 | "osm": { 13 | "type": "raster", 14 | "attribution": "© OpenStreetMap contributors.", 15 | "tileSize": 256, 16 | "tiles": [ 17 | "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", 18 | "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png", 19 | "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png" 20 | ] 21 | } 22 | }, 23 | "layers": [ 24 | { 25 | "id": "background", 26 | "type": "background", 27 | "paint": { 28 | "background-color": "rgba(255,255,0,0.2)" 29 | } 30 | }, 31 | { 32 | "id": "osm", 33 | "type": "raster", 34 | "source": "osm" 35 | }, 36 | { 37 | "id": "water_areas_fill", 38 | "type": "fill", 39 | "source": "water_areas", 40 | "paint": { 41 | "fill-color": "#020E5D", 42 | "fill-opacity": 0.8 43 | } 44 | }, 45 | { 46 | "id": "water_areas_line", 47 | "type": "line", 48 | "source": "water_areas", 49 | "paint": { 50 | "line-color": "white" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /examples/data/geojson-wms.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states", 4 | "center": [-122.19952899999998, 51.920367528011525], 5 | "zoom": 3, 6 | "glyphs": "{fontstack}/{range}", 7 | "sources": { 8 | "states": { 9 | "type": "geojson", 10 | "data": "./states.geojson" 11 | }, 12 | "osm": { 13 | "type": "raster", 14 | "attribution": "© OpenStreetMap contributors.", 15 | "tileSize": 256, 16 | "tiles": [ 17 | "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", 18 | "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png", 19 | "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png" 20 | ] 21 | }, 22 | "spatial-org-osm": { 23 | "type": "vector", 24 | "url": "https://tegola-osm-demo.go-spatial.org/v1/capabilities/osm.json" 25 | } 26 | }, 27 | "layers": [ 28 | { 29 | "id": "background", 30 | "type": "background", 31 | "paint": { 32 | "background-color": "rgba(255,255,0,0.2)" 33 | } 34 | }, 35 | { 36 | "id": "osm", 37 | "type": "raster", 38 | "source": "osm" 39 | }, 40 | { 41 | "id": "land", 42 | "type": "fill", 43 | "source": "spatial-org-osm", 44 | "source-layer": "land", 45 | "minzoom": 0, 46 | "maxzoom": 24, 47 | "paint": { 48 | "fill-color": "rgba(247, 246, 241, 0.6)" 49 | } 50 | }, 51 | { 52 | "id": "states", 53 | "type": "fill", 54 | "source": "states", 55 | "paint": { 56 | "fill-color": "#A6CEE3", 57 | "fill-opacity": 0.4 58 | } 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /examples/data/geojson.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states", 4 | "center": [-122.19952899999998, 51.920367528011525], 5 | "zoom": 3, 6 | "glyphs": "{fontstack}/{range}", 7 | "sources": { 8 | "states": { 9 | "type": "geojson", 10 | "data": "./states.geojson" 11 | } 12 | }, 13 | "layers": [ 14 | { 15 | "id": "background", 16 | "type": "background", 17 | "paint": { 18 | "background-color": "rgba(0,0,0,0)" 19 | } 20 | }, 21 | { 22 | "id": "population_lt_2m", 23 | "type": "fill", 24 | "source": "states", 25 | "filter": ["<=", "PERSONS", 2000000], 26 | "paint": { 27 | "fill-color": "#A6CEE3", 28 | "fill-opacity": 0.7 29 | } 30 | }, 31 | { 32 | "id": "2m_lt_population_lte_4m", 33 | "type": "fill", 34 | "source": "states", 35 | "filter": ["all", [">", "PERSONS", 2000000], ["<=", "PERSONS", 4000000]], 36 | "paint": { 37 | "fill-color": "#0F78B4", 38 | "fill-opacity": 0.7 39 | } 40 | }, 41 | { 42 | "id": "population_gt_4m", 43 | "type": "fill", 44 | "source": "states", 45 | "filter": [">", "PERSONS", 4000000], 46 | "paint": { 47 | "fill-color": "#B2DF8A", 48 | "fill-opacity": 0.7 49 | } 50 | }, 51 | { 52 | "id": "state_outlines", 53 | "type": "line", 54 | "source": "states", 55 | "paint": { 56 | "line-color": "#8cadbf", 57 | "line-width": 0.1 58 | } 59 | }, 60 | { 61 | "id": "state_abbreviations", 62 | "type": "symbol", 63 | "source": "states", 64 | "minzoom": 4, 65 | "maxzoom": 5, 66 | "layout": { 67 | "text-field": "{STATE_ABBR}", 68 | "text-size": 12, 69 | "text-font": ["Arial Normal", "sans-serif Normal"] 70 | } 71 | }, 72 | { 73 | "id": "state_names", 74 | "type": "symbol", 75 | "source": "states", 76 | "minzoom": 5, 77 | "layout": { 78 | "text-field": ["concat", ["get", "STATE_ABBR"], "\n", ["get", "STATE_NAME"]], 79 | "text-size": 12, 80 | "text-font": ["Arial Normal", "sans-serif Normal"] 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /examples/data/polygons-style.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "Wasserschongebiete": { 5 | "type": "geojson", 6 | "data": "https://sos-at-vie-1.exo.io/w3geo/waldgis/data/Wasserschongebiete.geojson" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "Wasserschongebiete", 12 | "type": "fill", 13 | "source": "Wasserschongebiete", 14 | "paint": { 15 | "fill-color": "#37c0eb", 16 | "fill-outline-color": "#4444ce" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /examples/data/sprites-bw.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 20, "pixelRatio": 1, "x": 0, "height": 20}, "amenity_firestation": {"y": 0, "width": 50, "pixelRatio": 1, "x": 20, "height": 50}} -------------------------------------------------------------------------------- /examples/data/sprites-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/examples/data/sprites-bw.png -------------------------------------------------------------------------------- /examples/data/sprites-bw@2x.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 40, "pixelRatio": 2, "x": 0, "height": 40}, "amenity_firestation": {"y": 0, "width": 100, "pixelRatio": 2, "x": 40, "height": 100}} 2 | -------------------------------------------------------------------------------- /examples/data/sprites-bw@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/examples/data/sprites-bw@2x.png -------------------------------------------------------------------------------- /examples/data/sprites.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 20, "pixelRatio": 1, "x": 0, "height": 20}, "amenity_firestation": {"y": 0, "width": 50, "pixelRatio": 1, "x": 20, "height": 50}} -------------------------------------------------------------------------------- /examples/data/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/examples/data/sprites.png -------------------------------------------------------------------------------- /examples/data/sprites@2x.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 40, "pixelRatio": 2, "x": 0, "height": 40}, "amenity_firestation": {"y": 0, "width": 100, "pixelRatio": 2, "x": 40, "height": 100}} 2 | -------------------------------------------------------------------------------- /examples/data/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/examples/data/sprites@2x.png -------------------------------------------------------------------------------- /examples/data/states.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states", 4 | "glyphs": "{fontstack}/{range}", 5 | "sources": { 6 | "states": { 7 | "type": "geojson", 8 | "data": "./states.geojson" 9 | } 10 | }, 11 | "layers": [ 12 | { 13 | "id": "background", 14 | "type": "background", 15 | "paint": { 16 | "background-color": "rgba(0,0,0,0)" 17 | } 18 | }, 19 | { 20 | "id": "population_lt_2m", 21 | "type": "fill", 22 | "source": "states", 23 | "filter": ["<=", "PERSONS", 2000000], 24 | "paint": { 25 | "fill-color": "#A6CEE3", 26 | "fill-opacity": 0.7 27 | } 28 | }, 29 | { 30 | "id": "2m_lt_population_lte_4m", 31 | "type": "fill", 32 | "source": "states", 33 | "filter": ["all", [">", "PERSONS", 2000000], ["<=", "PERSONS", 4000000]], 34 | "paint": { 35 | "fill-color": "#0F78B4", 36 | "fill-opacity": 0.7 37 | } 38 | }, 39 | { 40 | "id": "population_gt_4m", 41 | "type": "fill", 42 | "source": "states", 43 | "filter": [">", "PERSONS", 4000000], 44 | "paint": { 45 | "fill-color": "#B2DF8A", 46 | "fill-opacity": 0.7 47 | } 48 | }, 49 | { 50 | "id": "state_outlines", 51 | "type": "line", 52 | "source": "states", 53 | "paint": { 54 | "line-color": "#8cadbf", 55 | "line-width": 0.1 56 | } 57 | }, 58 | { 59 | "id": "state_abbreviations", 60 | "type": "symbol", 61 | "source": "states", 62 | "minzoom": 4, 63 | "maxzoom": 5, 64 | "layout": { 65 | "text-field": "{STATE_ABBR}", 66 | "text-size": 12, 67 | "text-font": ["Arial Normal", "sans-serif Normal"] 68 | } 69 | }, 70 | { 71 | "id": "state_names", 72 | "type": "symbol", 73 | "source": "states", 74 | "minzoom": 5, 75 | "layout": { 76 | "text-field": ["concat", ["get", "STATE_ABBR"], "\n", ["get", "STATE_NAME"]], 77 | "text-size": 12, 78 | "text-font": ["Arial Normal", "sans-serif Normal"] 79 | } 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /examples/data/tilejson.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "tilejson", 4 | "center": [0, 0], 5 | "zoom": 2, 6 | "sources": { 7 | "tilejson": { 8 | "type": "raster", 9 | "url": "https://maps.gnosis.earth/ogcapi/collections/NaturalEarth:raster:HYP_HR_SR_OB_DR/map/tiles/WebMercatorQuad?f=tilejson" 10 | } 11 | }, 12 | "layers": [ 13 | { 14 | "id": "background", 15 | "type": "background", 16 | "paint": { 17 | "background-color": "rgba(0,0,0,0)" 18 | } 19 | }, 20 | { 21 | "id": "tilejson-layer", 22 | "type": "raster", 23 | "source": "tilejson" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/data/wms.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states-wms", 4 | "center": [-98.78906130124426, 37.92686191312036], 5 | "zoom": 4, 6 | "sources": { 7 | "osm": { 8 | "type": "raster", 9 | "attribution": "© OpenStreetMap contributors.", 10 | "tileSize": 256, 11 | "tiles": [ 12 | "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", 13 | "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png", 14 | "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png" 15 | ] 16 | }, 17 | "states": { 18 | "type": "raster", 19 | "maxzoom": 12, 20 | "tileSize": 256, 21 | "tiles": ["https://ahocevar.com/geoserver/gwc/service/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image/png&SRS=EPSG:900913&LAYERS=topp:states&STYLES=&WIDTH=256&HEIGHT=256&BBOX={bbox-epsg-3857}"] 22 | } 23 | }, 24 | "layers": [ 25 | { 26 | "id": "background", 27 | "type": "background", 28 | "paint": { 29 | "background-color": "rgba(0,0,0,0)" 30 | } 31 | }, 32 | { 33 | "id": "osm", 34 | "type": "raster", 35 | "source": "osm" 36 | }, 37 | { 38 | "id": "states-wms", 39 | "type": "raster", 40 | "source": "states" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /examples/esri-4326.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply( 5 | 'map', 6 | 'https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_GCS_v2/VectorTileServer/resources/styles/', 7 | { 8 | projection: 'EPSG:4326', 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /examples/esri-transformrequest.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | olms( 5 | 'map', 6 | 'https://www.arcgis.com/sharing/rest/content/items/2afe5b807fa74006be6363fd243ffb30/resources/styles/root.json', 7 | { 8 | transformRequest(url, type) { 9 | if (type === 'Source') { 10 | return new Request( 11 | url.replace('/VectorTileServer', '/VectorTileServer/'), 12 | ); 13 | } 14 | }, 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /examples/geojson-featurestate.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms, {setFeatureState} from 'ol-mapbox-style'; 3 | 4 | const styleUrl = 'data/geojson.json'; 5 | 6 | fetch(styleUrl) 7 | .then((response) => response.json()) 8 | .then((glStyle) => { 9 | glStyle.layers.push({ 10 | 'id': 'state-hover', 11 | 'type': 'fill', 12 | 'source': 'states', 13 | 'paint': { 14 | 'fill-color': 'red', 15 | 'fill-opacity': [ 16 | 'case', 17 | ['boolean', ['feature-state', 'hover'], false], 18 | 0.5, 19 | 0, 20 | ], 21 | }, 22 | }); 23 | return olms('map', glStyle, {styleUrl: styleUrl}); 24 | }) 25 | .then((map) => { 26 | let hoveredStateId = null; 27 | map.on('pointermove', function (evt) { 28 | const features = map.getFeaturesAtPixel(evt.pixel); 29 | if (features.length > 0) { 30 | if (hoveredStateId !== null) { 31 | setFeatureState(map, {source: 'states', id: hoveredStateId}, null); 32 | } 33 | hoveredStateId = features[0].getId(); 34 | setFeatureState( 35 | map, 36 | {source: 'states', id: hoveredStateId}, 37 | {hover: true}, 38 | ); 39 | } else if (hoveredStateId !== null) { 40 | setFeatureState(map, {source: 'states', id: hoveredStateId}, null); 41 | hoveredStateId = null; 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/geojson-inline.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply('map', 'data/geojson-inline.json'); 5 | -------------------------------------------------------------------------------- /examples/geojson-layer.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {Map, View} from 'ol'; 3 | import VectorLayer from 'ol/layer/Vector.js'; 4 | import {fromLonLat} from 'ol/proj.js'; 5 | import {applyStyle} from 'ol-mapbox-style'; 6 | 7 | const layer = new VectorLayer(); 8 | applyStyle(layer, 'data/geojson.json'); 9 | new Map({ 10 | target: 'map', 11 | layers: [layer], 12 | view: new View({ 13 | center: fromLonLat([-122.19952899999998, 51.920367528011525]), 14 | zoom: 3, 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /examples/geojson-wfs.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply('map', 'data/geojson-wfs.json'); 5 | -------------------------------------------------------------------------------- /examples/geojson.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply('map', 'data/geojson.json'); 5 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ol-mapbox-style Examples 9 | 14 | 15 | 16 |

ol-mapbox-style Examples

17 |

To see the source code for the examples below, go to https://github.com/openlayers/ol-mapbox-style/tree/main/examples/.

18 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/mapbox.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | let key = document.cookie.replace( 5 | /(?:(?:^|.*;\s*)mapbox_access_token\s*\=\s*([^;]*).*$)|^.*$/, 6 | '$1', 7 | ); 8 | if (!key) { 9 | key = window.prompt('Enter your Mapbox API access token:'); 10 | if (key) { 11 | document.cookie = 12 | 'mapbox_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; 13 | } 14 | } 15 | 16 | olms('map', 'mapbox://styles/mapbox/streets-v12', {accessToken: key}); 17 | -------------------------------------------------------------------------------- /examples/maptiler-hillshading.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | let key = document.cookie.replace( 5 | /(?:(?:^|.*;\s*)maptiler_access_token\s*\=\s*([^;]*).*$)|^.*$/, 6 | '$1', 7 | ); 8 | if (!key) { 9 | key = window.prompt('Enter your MapTiler API access token:'); 10 | document.cookie = 11 | 'maptiler_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; 12 | } 13 | 14 | fetch(`https://api.maptiler.com/maps/outdoor-v2/style.json?key=${key}`) 15 | .then((response) => response.json()) 16 | .then((style) => { 17 | olms( 18 | 'map', 19 | Object.assign({}, style, { 20 | center: [13.783578, 47.609499], 21 | zoom: 11, 22 | }), 23 | { 24 | webfont: 25 | 'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}', 26 | }, 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/openmaptiles-layer.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {Map, View} from 'ol'; 3 | import VectorTileLayer from 'ol/layer/VectorTile.js'; 4 | import {applyBackground, applyStyle} from 'ol-mapbox-style'; 5 | 6 | const baseUrl = 'https://api.maptiler.com/maps/basic-v2/style.json'; 7 | 8 | let key = document.cookie.replace( 9 | /(?:(?:^|.*;\s*)maptiler_access_token\s*\=\s*([^;]*).*$)|^.*$/, 10 | '$1', 11 | ); 12 | if (!key) { 13 | key = window.prompt('Enter your MapTiler API access token:'); 14 | document.cookie = 15 | 'maptiler_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; 16 | } 17 | const styleUrl = baseUrl + '?key=' + key; 18 | 19 | const layer = new VectorTileLayer({ 20 | declutter: true, 21 | }); 22 | applyStyle(layer, styleUrl); 23 | applyBackground(layer, styleUrl); 24 | new Map({ 25 | target: 'map', 26 | layers: [layer], 27 | view: new View({ 28 | center: [0, 0], 29 | zoom: 2, 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /examples/openmaptiles.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | const baseUrl = 'https://api.maptiler.com/maps/basic-v2/style.json'; 5 | 6 | let key = document.cookie.replace( 7 | /(?:(?:^|.*;\s*)maptiler_access_token\s*\=\s*([^;]*).*$)|^.*$/, 8 | '$1', 9 | ); 10 | if (!key) { 11 | key = window.prompt('Enter your MapTiler API access token:'); 12 | document.cookie = 13 | 'maptiler_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; 14 | } 15 | 16 | olms('map', baseUrl + '?key=' + key); 17 | -------------------------------------------------------------------------------- /examples/pmtiles.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | import {fetch} from 'pmtiles-protocol'; 4 | 5 | apply('map', 'data/protomaps-dark-style.json', { 6 | transformRequest: (url) => fetch(url), 7 | }); 8 | -------------------------------------------------------------------------------- /examples/sdf-sprites.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | const baseUrl = 'https://api.maptiler.com/maps/test-bright/style.json'; 5 | 6 | let key = document.cookie.replace( 7 | /(?:(?:^|.*;\s*)maptiler_access_token\s*\=\s*([^;]*).*$)|^.*$/, 8 | '$1', 9 | ); 10 | if (!key) { 11 | key = window.prompt('Enter your MapTiler API access token:'); 12 | document.cookie = 13 | 'maptiler_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; 14 | } 15 | 16 | olms('map', baseUrl + '?key=' + key); 17 | -------------------------------------------------------------------------------- /examples/stylefunction.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import Map from 'ol/Map.js'; 3 | import View from 'ol/View.js'; 4 | import GeoJsonFormat from 'ol/format/GeoJSON.js'; 5 | import VectorLayer from 'ol/layer/Vector.js'; 6 | import VectorSource from 'ol/source/Vector.js'; 7 | 8 | import {stylefunction} from 'ol-mapbox-style'; 9 | 10 | const layer = new VectorLayer({ 11 | declutter: true, 12 | source: new VectorSource({ 13 | format: new GeoJsonFormat(), 14 | url: 'data/states.geojson', 15 | }), 16 | }); 17 | 18 | const map = new Map({ 19 | target: 'map', 20 | view: new View({ 21 | center: [-13603186.115192635, 6785744.563386], 22 | zoom: 2, 23 | }), 24 | }); 25 | 26 | fetch('data/states.json') 27 | .then((r) => r.json()) 28 | .then((glStyle) => { 29 | stylefunction(layer, glStyle, 'states'); 30 | if (map.getLayers().getArray().indexOf(layer) === -1) { 31 | map.addLayer(layer); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /examples/terrarium-hillshading.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | const style = { 5 | version: 8, 6 | name: 'Terrarium', 7 | center: [13.783578, 47.609499], 8 | zoom: 11, 9 | sources: { 10 | osm: { 11 | type: 'raster', 12 | attribution: 13 | '© OpenStreetMap contributors.', 14 | tileSize: 256, 15 | tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], 16 | maxzoom: 19, 17 | }, 18 | terrarium: { 19 | type: 'raster-dem', 20 | attribution: 21 | 'Data sources and attribution', 22 | tileSize: 256, 23 | tiles: [ 24 | 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png', 25 | ], 26 | maxzoom: 15, 27 | encoding: 'terrarium', 28 | }, 29 | }, 30 | layers: [ 31 | { 32 | id: 'osm', 33 | type: 'raster', 34 | source: 'osm', 35 | }, 36 | { 37 | id: 'hillshade', 38 | type: 'hillshade', 39 | source: 'terrarium', 40 | paint: { 41 | 'hillshade-accent-color': '#D8E8CF', 42 | 'hillshade-exaggeration': { 43 | stops: [ 44 | [6, 0.4], 45 | [14, 0.35], 46 | [18, 0.25], 47 | ], 48 | }, 49 | 'hillshade-shadow-color': '#6C6665', 50 | 'hillshade-highlight-color': '#B8AAA3', 51 | }, 52 | }, 53 | ], 54 | }; 55 | 56 | olms('map', style); 57 | -------------------------------------------------------------------------------- /examples/tilejson-vectortile.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply('map', ' https://demo.tegola.io/styles/hot-osm.json'); 5 | -------------------------------------------------------------------------------- /examples/tilejson.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import {apply} from 'ol-mapbox-style'; 3 | 4 | apply('map', 'data/tilejson.json'); 5 | -------------------------------------------------------------------------------- /examples/versatiles.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | const baseUrl = 5 | 'https://tiles.versatiles.org/assets/styles/colorful/style.json'; 6 | 7 | olms('map', baseUrl); 8 | -------------------------------------------------------------------------------- /examples/wms.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css'; 2 | import olms from 'ol-mapbox-style'; 3 | 4 | olms('map', 'data/wms.json'); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ol-mapbox-style", 3 | "version": "13.0.1", 4 | "description": "Create OpenLayers maps from Mapbox Style objects", 5 | "type": "module", 6 | "browser": "src/index.js", 7 | "main": "dist/olms.js", 8 | "module": "src/index.js", 9 | "types": "dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./src/index.js", 13 | "require": "./dist/olms.js", 14 | "types": "./dist/index.d.ts" 15 | } 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/openlayers/ol-mapbox-style.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/openlayers/ol-mapbox-style/issues" 23 | }, 24 | "homepage": "https://openlayers.org/ol-mapbox-style/", 25 | "keywords": [ 26 | "openlayers", 27 | "mapbox", 28 | "maplibre", 29 | "vector tiles" 30 | ], 31 | "license": "BSD-2-Clause", 32 | "scripts": { 33 | "start": "webpack serve --config ./webpack.config.examples.cjs", 34 | "prepare": "npm run doc && npm run build", 35 | "build": "tsc --project tsconfig-build.json && rollup -c && webpack-cli --mode=production --config ./webpack.config.examples.cjs", 36 | "doc": "typedoc --plugin typedoc-plugin-missing-exports src/index.js --excludeExternals --tsconfig tsconfig-typecheck.json --out ./_site", 37 | "karma": "karma start test/karma.conf.cjs", 38 | "lint": "eslint test examples src", 39 | "typecheck": "tsc --project tsconfig-typecheck.json", 40 | "pretest": "npm run lint && npm run typecheck", 41 | "test": "npm run karma -- --single-run --log-level error" 42 | }, 43 | "dependencies": { 44 | "@maplibre/maplibre-gl-style-spec": "^23.1.0", 45 | "mapbox-to-css-font": "^3.2.0" 46 | }, 47 | "peerDependencies": { 48 | "ol": "*" 49 | }, 50 | "devDependencies": { 51 | "@jsdevtools/coverage-istanbul-loader": "^3.0.5", 52 | "@openlayers/eslint-plugin": "^4.0.0", 53 | "@rollup/plugin-commonjs": "^28.0.3", 54 | "@rollup/plugin-node-resolve": "^16.0.0", 55 | "@rollup/plugin-terser": "^0.4.4", 56 | "@types/arcgis-rest-api": "^10.4.4", 57 | "@types/mocha": "^10.0.0", 58 | "@types/offscreencanvas": "^2019.6.4", 59 | "@types/topojson-specification": "^1.0.1", 60 | "copy-webpack-plugin": "^13.0.0", 61 | "cross-env": "^7.0.3", 62 | "css-loader": "^7.0.0", 63 | "deep-freeze": "0.0.1", 64 | "eslint": "^9.19.0", 65 | "eslint-config-openlayers": "^20.0.0", 66 | "globals": "^16.0.0", 67 | "html-webpack-plugin": "^5.5.0", 68 | "karma": "^6.4.4", 69 | "karma-chrome-launcher": "^3.2.0", 70 | "karma-coverage-istanbul-reporter": "^3.0.3", 71 | "karma-mocha": "^2.0.1", 72 | "karma-sourcemap-loader": "^0.4.0", 73 | "karma-webpack": "^5.0.0", 74 | "mapbox-gl-styles": "^2.0.2", 75 | "mini-css-extract-plugin": "^2.4.4", 76 | "mocha": "^11.1.0", 77 | "nanoassert": "^2.0.0", 78 | "pmtiles-protocol": "^1.0.5", 79 | "proj4": "^2.15.0", 80 | "puppeteer": "^24.2.0", 81 | "rollup": "^4.34.6", 82 | "should": "^13.2.3", 83 | "sinon": "^19.0.2", 84 | "style-loader": "^4.0.0", 85 | "typedoc": "^0.28.3", 86 | "typedoc-plugin-missing-exports": "^4.0.0", 87 | "typescript": "^5.7.3", 88 | "webpack": "^5.62.1", 89 | "webpack-cli": "^6.0.1", 90 | "webpack-dev-server": "^5.0.4" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {fileURLToPath} from 'url'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import terser from '@rollup/plugin-terser'; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = [ 9 | { 10 | external: (id) => /ol(\/.+)?$/.test(id), 11 | input: `${__dirname}/src/index.js`, 12 | output: { 13 | name: 'olms', 14 | globals: (id) => 15 | /ol(\/.+)?$/.test(id) 16 | ? id.replace(/\.js$/, '').split('/').join('.') 17 | : id, 18 | file: `${__dirname}/dist/olms.js`, 19 | format: 'umd', 20 | sourcemap: true, 21 | plugins: [terser()], 22 | }, 23 | plugins: [ 24 | resolve({ 25 | browser: true, 26 | preferBuiltins: false, 27 | }), 28 | commonjs(), 29 | ], 30 | }, 31 | ]; 32 | export default config; 33 | -------------------------------------------------------------------------------- /src/MapboxVectorLayer.js: -------------------------------------------------------------------------------- 1 | import BaseEvent from 'ol/events/Event.js'; 2 | import EventType from 'ol/events/EventType.js'; 3 | import MVT from 'ol/format/MVT.js'; 4 | import VectorTileLayer from 'ol/layer/VectorTile.js'; 5 | import VectorTileSource from 'ol/source/VectorTile.js'; 6 | import {applyBackground, applyStyle} from './apply.js'; 7 | 8 | /** @typedef {import("ol/Map.js").default} Map */ 9 | 10 | /** 11 | * Event emitted on configuration or loading error. 12 | */ 13 | class ErrorEvent extends BaseEvent { 14 | /** 15 | * @param {Error} error error object. 16 | */ 17 | constructor(error) { 18 | super(EventType.ERROR); 19 | 20 | /** 21 | * @type {Error} 22 | */ 23 | this.error = error; 24 | } 25 | } 26 | 27 | /** 28 | * @typedef {Object} Options 29 | * @property {string} styleUrl The URL of the Mapbox/MapLibre Style object to use for this layer. For a 30 | * style created with Mapbox Studio and hosted on Mapbox, this will look like 31 | * 'mapbox://styles/you/your-style'. 32 | * @property {string} [accessToken] The access token for your Mapbox/MapLibre style. This has to be provided 33 | * for `mapbox://` style urls. For `https://` and other urls, any access key must be the last query 34 | * parameter of the style url. 35 | * @property {string} [source] If your style uses more than one source, you need to use either the 36 | * `source` property or the `layers` property to limit rendering to a single vector source. The 37 | * `source` property corresponds to the id of a vector source in your Mapbox/MapLibre style. 38 | * @property {Array} [layers] Limit rendering to the list of included layers. All layers 39 | * must share the same vector source. If your style uses more than one source, you need to use 40 | * either the `source` property or the `layers` property to limit rendering to a single vector 41 | * source. 42 | * @property {boolean} [declutter=true] Declutter images and text. Decluttering is applied to all 43 | * image and text styles of all Vector and VectorTile layers that have set this to `true`. The priority 44 | * is defined by the z-index of the layer, the `zIndex` of the style and the render order of features. 45 | * Higher z-index means higher priority. Within the same z-index, a feature rendered before another has 46 | * higher priority. 47 | * 48 | * As an optimization decluttered features from layers with the same `className` are rendered above 49 | * the fill and stroke styles of all of those layers regardless of z-index. To opt out of this 50 | * behavior and place declutterd features with their own layer configure the layer with a `className` 51 | * other than `ol-layer`. 52 | * @property {import("ol/layer/Base.js").BackgroundColor|false} [background] Background color for the layer. 53 | * If not specified, the background from the Mapbox/MapLibre Style object will be used. Set to `false` to prevent 54 | * the Mapbox/MapLibre style's background from being used. 55 | * @property {string} [className='ol-layer'] A CSS class name to set to the layer element. 56 | * @property {number} [opacity=1] Opacity (0, 1). 57 | * @property {boolean} [visible=true] Visibility. 58 | * @property {import("ol/extent.js").Extent} [extent] The bounding extent for layer rendering. The layer will not be 59 | * rendered outside of this extent. 60 | * @property {number} [zIndex] The z-index for layer rendering. At rendering time, the layers 61 | * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed 62 | * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()` 63 | * method was used. 64 | * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be 65 | * visible. 66 | * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will 67 | * be visible. If neither `maxResolution` nor `minZoom` are defined, the layer's `maxResolution` will 68 | * match the style source's `minzoom`. 69 | * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will 70 | * be visible. If neither `maxResolution` nor `minZoom` are defined, the layer's `minZoom` will match 71 | * the style source's `minzoom`. 72 | * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will 73 | * be visible. 74 | * @property {import("ol/render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting 75 | * features before rendering. By default features are drawn in the order that they are created. Use 76 | * `null` to avoid the sort, but get an undefined draw order. 77 | * @property {number} [renderBuffer=100] The buffer in pixels around the tile extent used by the 78 | * renderer when getting features from the vector tile for the rendering or hit-detection. 79 | * Recommended value: Vector tiles are usually generated with a buffer, so this value should match 80 | * the largest possible buffer of the used tiles. It should be at least the size of the largest 81 | * point symbol or line width. 82 | * @property {import("ol/layer/VectorTile.js").VectorTileRenderType} [renderMode='hybrid'] Render mode for vector tiles: 83 | * * `'hybrid'`: Polygon and line elements are rendered as images, so pixels are scaled during zoom 84 | * animations. Point symbols and texts are accurately rendered as vectors and can stay upright on 85 | * rotated views. 86 | * * `'vector'`: Everything is rendered as vectors. Use this mode for improved performance on vector 87 | * tile layers with only a few rendered features (e.g. for highlighting a subset of features of 88 | * another layer with the same source). 89 | * @property {import("ol/Map.js").default} [map] Sets the layer as overlay on a map. The map will not manage 90 | * this layer in its layers collection, and the layer will be rendered on top. This is useful for 91 | * temporary layers. The standard way to add a layer to a map and have it managed by the map is to 92 | * use `map.addLayer()`. 93 | * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will be 94 | * recreated during animations. This means that no vectors will be shown clipped, but the setting 95 | * will have a performance impact for large amounts of vector data. When set to `false`, batches 96 | * will be recreated when no animation is active. 97 | * @property {boolean} [updateWhileInteracting=false] When set to `true`, feature batches will be 98 | * recreated during interactions. See also `updateWhileAnimating`. 99 | * @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0` 100 | * means no preloading. 101 | * @property {boolean} [useInterimTilesOnError=true] Use interim tiles on error. 102 | * @property {Object} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`. 103 | */ 104 | 105 | /** 106 | * ```js 107 | * import {MapboxVectorLayer} from 'ol-mapbox-style'; 108 | * ``` 109 | * A vector tile layer based on a Mapbox/MapLibre style that uses a single vector source. Configure 110 | * the layer with the `styleUrl` and `accessToken` shown in Mapbox Studio's share panel. 111 | * If the style uses more than one source, use the `source` property to choose a single 112 | * vector source. If you want to render a subset of the layers in the style, use the `layers` 113 | * property (all layers must share the same vector source). See the constructor options for 114 | * more detail. 115 | * 116 | * const map = new Map({ 117 | * view: new View({ 118 | * center: [0, 0], 119 | * zoom: 1, 120 | * }), 121 | * layers: [ 122 | * new MapboxVectorLayer({ 123 | * styleUrl: 'mapbox://styles/mapbox/bright-v9', 124 | * accessToken: 'your-mapbox-access-token-here', 125 | * }), 126 | * ], 127 | * target: 'map', 128 | * }); 129 | * 130 | * On configuration or loading error, the layer will trigger an `'error'` event. Listeners 131 | * will receive an object with an `error` property that can be used to diagnose the problem. 132 | * 133 | * Instances of this class emit an `error` event when an error occurs during style loading: 134 | * 135 | * layer.on('error', function() { 136 | * console.error('Error loading style'); 137 | * } 138 | * 139 | * **Note for users of the full build**: The `MapboxVectorLayer` requires the 140 | * [ol-mapbox-style](https://github.com/openlayers/ol-mapbox-style) library to be loaded as well. 141 | * 142 | * @param {Options} options Options. 143 | * @extends {VectorTileLayer} 144 | */ 145 | export default class MapboxVectorLayer extends VectorTileLayer { 146 | /** 147 | * @param {Options} options Layer options. At a minimum, `styleUrl` and `accessToken` 148 | * must be provided. 149 | */ 150 | constructor(options) { 151 | const declutter = 'declutter' in options ? options.declutter : true; 152 | const source = new VectorTileSource({ 153 | state: 'loading', 154 | format: new MVT({layerName: 'mvt:layer'}), 155 | }); 156 | 157 | super({ 158 | source: source, 159 | background: options.background === false ? null : options.background, 160 | declutter: declutter, 161 | className: options.className, 162 | opacity: options.opacity, 163 | visible: options.visible, 164 | zIndex: options.zIndex, 165 | minResolution: options.minResolution, 166 | maxResolution: options.maxResolution, 167 | minZoom: options.minZoom, 168 | maxZoom: options.maxZoom, 169 | renderOrder: options.renderOrder, 170 | renderBuffer: options.renderBuffer, 171 | renderMode: options.renderMode, 172 | map: options.map, 173 | updateWhileAnimating: options.updateWhileAnimating, 174 | updateWhileInteracting: options.updateWhileInteracting, 175 | preload: options.preload, 176 | useInterimTilesOnError: options.useInterimTilesOnError, 177 | properties: options.properties, 178 | }); 179 | 180 | if (options.accessToken) { 181 | this.accessToken = options.accessToken; 182 | } 183 | const url = options.styleUrl; 184 | const promises = [ 185 | applyStyle(this, url, options.layers || options.source, { 186 | accessToken: this.accessToken, 187 | }), 188 | ]; 189 | if (this.getBackground() === undefined) { 190 | promises.push( 191 | applyBackground(this, options.styleUrl, { 192 | accessToken: this.accessToken, 193 | }), 194 | ); 195 | } 196 | Promise.all(promises) 197 | .then(() => { 198 | source.setState('ready'); 199 | }) 200 | .catch((error) => { 201 | this.dispatchEvent(new ErrorEvent(error)); 202 | const source = this.getSource(); 203 | source.setState('error'); 204 | }); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | getStyleForLayer, 3 | recordStyleLayer, 4 | renderTransparent, 5 | stylefunction, 6 | } from './stylefunction.js'; 7 | export { 8 | addMapboxLayer, 9 | apply, 10 | apply as default, 11 | applyBackground, 12 | applyStyle, 13 | getFeatureState, 14 | getLayer, 15 | getLayers, 16 | getMapboxLayer, 17 | getSource, 18 | removeMapboxLayer, 19 | setFeatureState, 20 | updateMapboxLayer, 21 | updateMapboxSource, 22 | } from './apply.js'; 23 | export {default as MapboxVectorLayer} from './MapboxVectorLayer.js'; 24 | -------------------------------------------------------------------------------- /src/mapbox.js: -------------------------------------------------------------------------------- 1 | const mapboxBaseUrl = 'https://api.mapbox.com'; 2 | 3 | /** 4 | * @typedef {Object} Sprite 5 | * @property {string} id Id of the sprite source. 6 | * @property {string} url URL to the sprite source. 7 | */ 8 | 9 | /** 10 | * Gets the path from a mapbox:// URL. 11 | * @param {string} url The Mapbox URL. 12 | * @return {string} The path. 13 | * @private 14 | */ 15 | export function getMapboxPath(url) { 16 | const startsWith = 'mapbox://'; 17 | if (url.indexOf(startsWith) !== 0) { 18 | return ''; 19 | } 20 | return url.slice(startsWith.length); 21 | } 22 | 23 | /** 24 | * Normalizes legacy string-based or new-style array based sprite definitions into array-based. 25 | * @param {string|Array} sprite the sprite source. 26 | * @param {string} token The access token. 27 | * @param {string} styleUrl The style URL. 28 | * @return {Array} An array of sprite definitions with normalized URLs. 29 | * @private 30 | */ 31 | export function normalizeSpriteDefinition(sprite, token, styleUrl) { 32 | if (typeof sprite === 'string') { 33 | return [ 34 | { 35 | 'id': 'default', 36 | 'url': normalizeSpriteUrl(sprite, token, styleUrl), 37 | }, 38 | ]; 39 | } 40 | 41 | for (const spriteObj of sprite) { 42 | spriteObj.url = normalizeSpriteUrl(spriteObj.url, token, styleUrl); 43 | } 44 | 45 | return sprite; 46 | } 47 | 48 | /** 49 | * Turns mapbox:// sprite URLs into resolvable URLs. 50 | * @param {string} url The sprite URL. 51 | * @param {string} token The access token. 52 | * @param {string} styleUrl The style URL. 53 | * @return {string} A resolvable URL. 54 | * @private 55 | */ 56 | export function normalizeSpriteUrl(url, token, styleUrl) { 57 | const mapboxPath = getMapboxPath(url); 58 | if (!mapboxPath) { 59 | return decodeURI(new URL(url, styleUrl).href); 60 | } 61 | const startsWith = 'sprites/'; 62 | if (mapboxPath.indexOf(startsWith) !== 0) { 63 | throw new Error(`unexpected sprites url: ${url}`); 64 | } 65 | const sprite = mapboxPath.slice(startsWith.length); 66 | 67 | return `${mapboxBaseUrl}/styles/v1/${sprite}/sprite?access_token=${token}`; 68 | } 69 | 70 | /** 71 | * Turns mapbox:// style URLs into resolvable URLs. 72 | * @param {string} url The style URL. 73 | * @param {string} token The access token. 74 | * @return {string} A resolvable URL. 75 | * @private 76 | */ 77 | export function normalizeStyleUrl(url, token) { 78 | const mapboxPath = getMapboxPath(url); 79 | if (!mapboxPath) { 80 | return decodeURI(new URL(url, location.href).href); 81 | } 82 | const startsWith = 'styles/'; 83 | if (mapboxPath.indexOf(startsWith) !== 0) { 84 | throw new Error(`unexpected style url: ${url}`); 85 | } 86 | const style = mapboxPath.slice(startsWith.length); 87 | 88 | return `${mapboxBaseUrl}/styles/v1/${style}?&access_token=${token}`; 89 | } 90 | 91 | const mapboxSubdomains = ['a', 'b', 'c', 'd']; 92 | 93 | /** 94 | * Turns mapbox:// source URLs into vector tile URL templates. 95 | * @param {string} url The source URL. 96 | * @param {string} token The access token. 97 | * @param {string} tokenParam The access token key. 98 | * @param {string} styleUrl The style URL. 99 | * @return {Array} A vector tile template. 100 | * @private 101 | */ 102 | export function normalizeSourceUrl(url, token, tokenParam, styleUrl) { 103 | const urlObject = new URL(url, styleUrl || location.href); 104 | const mapboxPath = getMapboxPath(url); 105 | if (!mapboxPath) { 106 | if (!token) { 107 | return [decodeURI(urlObject.href)]; 108 | } 109 | if (!urlObject.searchParams.has(tokenParam)) { 110 | urlObject.searchParams.set(tokenParam, token); 111 | } 112 | return [decodeURI(urlObject.href)]; 113 | } 114 | 115 | if (mapboxPath === 'mapbox.satellite') { 116 | const sizeFactor = window.devicePixelRatio >= 1.5 ? '@2x' : ''; 117 | return [ 118 | `https://api.mapbox.com/v4/${mapboxPath}/{z}/{x}/{y}${sizeFactor}.webp?access_token=${token}`, 119 | ]; 120 | } 121 | return mapboxSubdomains.map( 122 | (sub) => 123 | `https://${sub}.tiles.mapbox.com/v4/${mapboxPath}/{z}/{x}/{y}.vector.pbf?access_token=${token}`, 124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/shaders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a shaded relief image given elevation data. Uses a 3x3 3 | * neighborhood for determining slope and aspect. 4 | * @param {Array} inputs Array of input images. 5 | * @param {Object} data Data added in the "beforeoperations" event. 6 | * @return {ImageData} Output image. 7 | */ 8 | export function hillshade(inputs, data) { 9 | const elevationImage = inputs[0]; 10 | const width = elevationImage.width; 11 | const height = elevationImage.height; 12 | const elevationData = elevationImage.data; 13 | const shadeData = new Uint8ClampedArray(elevationData.length); 14 | const dp = data.resolution * 2; 15 | const maxX = width - 1; 16 | const maxY = height - 1; 17 | const pixel = [0, 0, 0, 0]; 18 | const twoPi = 2 * Math.PI; 19 | const halfPi = Math.PI / 2; 20 | const sunEl = (Math.PI * data.sunEl) / 180; 21 | const sunAz = (Math.PI * data.sunAz) / 180; 22 | const cosSunEl = Math.cos(sunEl); 23 | const sinSunEl = Math.sin(sunEl); 24 | const highlightColor = data.highlightColor; 25 | const shadowColor = data.shadowColor; 26 | const accentColor = data.accentColor; 27 | const encoding = data.encoding; 28 | 29 | let pixelX, 30 | pixelY, 31 | x0, 32 | x1, 33 | y0, 34 | y1, 35 | offset, 36 | z0, 37 | z1, 38 | dzdx, 39 | dzdy, 40 | slope, 41 | aspect, 42 | accent, 43 | scaled, 44 | shade, 45 | scaledAccentColor, 46 | compositeShadeColor, 47 | clamp, 48 | slopeScaleBase, 49 | scaledSlope, 50 | cosIncidence; 51 | 52 | function calculateElevation(pixel, encoding = 'mapbox') { 53 | // The method used to extract elevations from the DEM. 54 | // 55 | // The supported methods are the Mapbox format 56 | // (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000 57 | // and the Terrarium format 58 | // (red * 256 + green + blue / 256) - 32768 59 | // 60 | if (encoding === 'mapbox') { 61 | return (pixel[0] * 256 * 256 + pixel[1] * 256 + pixel[2]) * 0.1 - 10000; 62 | } 63 | if (encoding === 'terrarium') { 64 | return pixel[0] * 256 + pixel[1] + pixel[2] / 256 - 32768; 65 | } 66 | } 67 | for (pixelY = 0; pixelY <= maxY; ++pixelY) { 68 | y0 = pixelY === 0 ? 0 : pixelY - 1; 69 | y1 = pixelY === maxY ? maxY : pixelY + 1; 70 | for (pixelX = 0; pixelX <= maxX; ++pixelX) { 71 | x0 = pixelX === 0 ? 0 : pixelX - 1; 72 | x1 = pixelX === maxX ? maxX : pixelX + 1; 73 | 74 | // determine elevation for (x0, pixelY) 75 | offset = (pixelY * width + x0) * 4; 76 | pixel[0] = elevationData[offset]; 77 | pixel[1] = elevationData[offset + 1]; 78 | pixel[2] = elevationData[offset + 2]; 79 | pixel[3] = elevationData[offset + 3]; 80 | z0 = data.vert * calculateElevation(pixel, encoding); 81 | 82 | // determine elevation for (x1, pixelY) 83 | offset = (pixelY * width + x1) * 4; 84 | pixel[0] = elevationData[offset]; 85 | pixel[1] = elevationData[offset + 1]; 86 | pixel[2] = elevationData[offset + 2]; 87 | pixel[3] = elevationData[offset + 3]; 88 | z1 = data.vert * calculateElevation(pixel, encoding); 89 | 90 | dzdx = (z1 - z0) / dp; 91 | 92 | // determine elevation for (pixelX, y0) 93 | offset = (y0 * width + pixelX) * 4; 94 | pixel[0] = elevationData[offset]; 95 | pixel[1] = elevationData[offset + 1]; 96 | pixel[2] = elevationData[offset + 2]; 97 | pixel[3] = elevationData[offset + 3]; 98 | z0 = data.vert * calculateElevation(pixel, encoding); 99 | 100 | // determine elevation for (pixelX, y1) 101 | offset = (y1 * width + pixelX) * 4; 102 | pixel[0] = elevationData[offset]; 103 | pixel[1] = elevationData[offset + 1]; 104 | pixel[2] = elevationData[offset + 2]; 105 | pixel[3] = elevationData[offset + 3]; 106 | z1 = data.vert * calculateElevation(pixel, encoding); 107 | 108 | dzdy = (z1 - z0) / dp; 109 | 110 | aspect = Math.atan2(dzdy, -dzdx); 111 | if (aspect < 0) { 112 | aspect = halfPi - aspect; 113 | } else if (aspect > halfPi) { 114 | aspect = twoPi - aspect + halfPi; 115 | } else { 116 | aspect = halfPi - aspect; 117 | } 118 | 119 | // Bootstrap slope and corresponding incident values 120 | slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy)); 121 | cosIncidence = 122 | sinSunEl * Math.cos(slope) + 123 | cosSunEl * Math.sin(slope) * Math.cos(sunAz - aspect); 124 | accent = Math.cos(slope); 125 | // 255 for Hex colors 126 | scaled = 255 * cosIncidence; 127 | 128 | /* 129 | * The following is heavily inspired 130 | * by [Maplibre's equivalent WebGL shader](https://github.com/maplibre/maplibre-gl-js/blob/main/src/shaders/hillshade.fragment.glsl) 131 | */ 132 | 133 | // Forces given value to stay between two given extremes 134 | clamp = Math.min(Math.max(2 * data.sunEl, 0), 1); 135 | 136 | // Intensity basis for hillshade opacity 137 | slopeScaleBase = 1.875 - data.opacity * 1.75; 138 | // Intensity interpolation so that higher intensity values create more opaque hillshading 139 | scaledSlope = 140 | data.opacity !== 0.5 141 | ? halfPi * 142 | ((Math.pow(slopeScaleBase, slope) - 1) / 143 | (Math.pow(slopeScaleBase, halfPi) - 1)) 144 | : slope; 145 | 146 | // Accent hillshade color with given accentColor to emphasize rougher terrain 147 | scaledAccentColor = { 148 | r: (1 - accent) * accentColor.r * clamp * 255, 149 | g: (1 - accent) * accentColor.g * clamp * 255, 150 | b: (1 - accent) * accentColor.b * clamp * 255, 151 | a: (1 - accent) * accentColor.a * clamp * 255, 152 | }; 153 | 154 | // Allows highlight vs shadow discrimination 155 | shade = Math.abs((((aspect + sunAz) / Math.PI + 0.5) % 2) - 1); 156 | // Creates a composite color mix between highlight & shadow colors to emphasize slopes 157 | compositeShadeColor = { 158 | r: (highlightColor.r * (1 - shade) + shadowColor.r * shade) * scaled, 159 | g: (highlightColor.g * (1 - shade) + shadowColor.g * shade) * scaled, 160 | b: (highlightColor.b * (1 - shade) + shadowColor.b * shade) * scaled, 161 | a: (highlightColor.a * (1 - shade) + shadowColor.a * shade) * scaled, 162 | }; 163 | 164 | // Fill in result color value 165 | offset = (pixelY * width + pixelX) * 4; 166 | shadeData[offset] = 167 | scaledAccentColor.r * (1 - shade) + compositeShadeColor.r; 168 | shadeData[offset + 1] = 169 | scaledAccentColor.g * (1 - shade) + compositeShadeColor.g; 170 | shadeData[offset + 2] = 171 | scaledAccentColor.b * (1 - shade) + compositeShadeColor.b; 172 | // Key opacity on the scaledSlope to improve legibility by increasing higher elevation rates' contrast 173 | shadeData[offset + 3] = 174 | elevationData[offset + 3] * 175 | data.opacity * 176 | clamp * 177 | Math.sin(scaledSlope); 178 | } 179 | } 180 | 181 | return new ImageData(shadeData, width, height); 182 | } 183 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | import mb2css from 'mapbox-to-css-font'; 2 | import {checkedFonts} from 'ol/render/canvas.js'; 3 | import {createCanvas} from './util.js'; 4 | 5 | const hairSpacePool = Array(256).join('\u200A'); 6 | export function applyLetterSpacing(text, letterSpacing) { 7 | if (letterSpacing >= 0.05) { 8 | let textWithLetterSpacing = ''; 9 | const lines = text.split('\n'); 10 | const joinSpaceString = hairSpacePool.slice( 11 | 0, 12 | Math.round(letterSpacing / 0.1), 13 | ); 14 | for (let l = 0, ll = lines.length; l < ll; ++l) { 15 | if (l > 0) { 16 | textWithLetterSpacing += '\n'; 17 | } 18 | textWithLetterSpacing += lines[l].split('').join(joinSpaceString); 19 | } 20 | return textWithLetterSpacing; 21 | } 22 | return text; 23 | } 24 | 25 | let measureContext; 26 | function getMeasureContext() { 27 | if (!measureContext) { 28 | measureContext = createCanvas(1, 1).getContext('2d'); 29 | } 30 | return measureContext; 31 | } 32 | 33 | function measureText(text, letterSpacing) { 34 | return ( 35 | getMeasureContext().measureText(text).width + 36 | (text.length - 1) * letterSpacing 37 | ); 38 | } 39 | 40 | const measureCache = {}; 41 | checkedFonts.on('propertychange', () => { 42 | for (const key in measureCache) { 43 | delete measureCache[key]; 44 | } 45 | }); 46 | 47 | export function wrapText(text, font, em, letterSpacing) { 48 | if (text.indexOf('\n') !== -1) { 49 | const hardLines = text.split('\n'); 50 | const lines = []; 51 | for (let i = 0, ii = hardLines.length; i < ii; ++i) { 52 | lines.push(wrapText(hardLines[i], font, em, letterSpacing)); 53 | } 54 | return lines.join('\n'); 55 | } 56 | const key = em + ',' + font + ',' + text + ',' + letterSpacing; 57 | let wrappedText = measureCache[key]; 58 | if (!wrappedText) { 59 | const words = text.split(' '); 60 | if (words.length > 1) { 61 | const ctx = getMeasureContext(); 62 | ctx.font = font; 63 | const oneEm = ctx.measureText('M').width; 64 | const maxWidth = oneEm * em; 65 | let line = ''; 66 | const lines = []; 67 | // Pass 1 - wrap lines to not exceed maxWidth 68 | for (let i = 0, ii = words.length; i < ii; ++i) { 69 | const word = words[i]; 70 | const testLine = line + (line ? ' ' : '') + word; 71 | if (measureText(testLine, letterSpacing) <= maxWidth) { 72 | line = testLine; 73 | } else { 74 | if (line) { 75 | lines.push(line); 76 | } 77 | line = word; 78 | } 79 | } 80 | if (line) { 81 | lines.push(line); 82 | } 83 | // Pass 2 - add lines with a width of less than 30% of maxWidth to the previous or next line 84 | for (let i = 0, ii = lines.length; i < ii && ii > 1; ++i) { 85 | const line = lines[i]; 86 | if (measureText(line, letterSpacing) < maxWidth * 0.35) { 87 | const prevWidth = 88 | i > 0 ? measureText(lines[i - 1], letterSpacing) : Infinity; 89 | const nextWidth = 90 | i < ii - 1 ? measureText(lines[i + 1], letterSpacing) : Infinity; 91 | lines.splice(i, 1); 92 | ii -= 1; 93 | if (prevWidth < nextWidth) { 94 | lines[i - 1] += ' ' + line; 95 | i -= 1; 96 | } else { 97 | lines[i] = line + ' ' + lines[i]; 98 | } 99 | } 100 | } 101 | // Pass 3 - try to fill 80% of maxWidth for each line 102 | for (let i = 0, ii = lines.length - 1; i < ii; ++i) { 103 | const line = lines[i]; 104 | const next = lines[i + 1]; 105 | if ( 106 | measureText(line, letterSpacing) > maxWidth * 0.7 && 107 | measureText(next, letterSpacing) < maxWidth * 0.6 108 | ) { 109 | const lineWords = line.split(' '); 110 | const lastWord = lineWords.pop(); 111 | if (measureText(lastWord, letterSpacing) < maxWidth * 0.2) { 112 | lines[i] = lineWords.join(' '); 113 | lines[i + 1] = lastWord + ' ' + next; 114 | } 115 | ii -= 1; 116 | } 117 | } 118 | wrappedText = lines.join('\n'); 119 | } else { 120 | wrappedText = text; 121 | } 122 | wrappedText = applyLetterSpacing(wrappedText, letterSpacing); 123 | measureCache[key] = wrappedText; 124 | } 125 | return wrappedText; 126 | } 127 | 128 | const webSafeFonts = [ 129 | 'Arial', 130 | 'Courier New', 131 | 'Times New Roman', 132 | 'Verdana', 133 | 'sans-serif', 134 | 'serif', 135 | 'monospace', 136 | 'cursive', 137 | 'fantasy', 138 | ]; 139 | 140 | const processedFontFamilies = {}; 141 | 142 | /** 143 | * @param {Array} fonts Fonts. 144 | * @param {string} [templateUrl] Template URL. 145 | * @return {Array} Processed fonts. 146 | * @private 147 | */ 148 | export function getFonts( 149 | fonts, 150 | templateUrl = 'https://cdn.jsdelivr.net/npm/@fontsource/{font-family}/{fontweight}{-fontstyle}.css', 151 | ) { 152 | let fontDescriptions; 153 | for (let i = 0, ii = fonts.length; i < ii; ++i) { 154 | const font = fonts[i]; 155 | if (font in processedFontFamilies) { 156 | continue; 157 | } 158 | processedFontFamilies[font] = true; 159 | const cssFont = mb2css(font, 16); 160 | const parts = cssFont.split(' '); 161 | if (!fontDescriptions) { 162 | fontDescriptions = []; 163 | } 164 | fontDescriptions.push([ 165 | parts.slice(3).join(' ').replace(/"/g, ''), 166 | parts[1], 167 | parts[0], 168 | ]); 169 | } 170 | if (!fontDescriptions) { 171 | return fonts; 172 | } 173 | 174 | (async () => { 175 | await document.fonts.ready; 176 | for (let i = 0, ii = fontDescriptions.length; i < ii; ++i) { 177 | const fontDescription = fontDescriptions[i]; 178 | const family = fontDescription[0]; 179 | if (webSafeFonts.includes(family)) { 180 | continue; 181 | } 182 | const weight = fontDescription[1]; 183 | const style = fontDescription[2]; 184 | const loaded = await document.fonts.load( 185 | `${style} ${weight} 16px "${family}"`, 186 | ); 187 | if ( 188 | !loaded.some( 189 | (f) => 190 | f.family.replace(/^['"]|['"]$/g, '').toLowerCase() === 191 | family.toLowerCase() && 192 | f.weight == weight && 193 | f.style === style, 194 | ) 195 | ) { 196 | const fontUrl = templateUrl 197 | .replace('{font-family}', family.replace(/ /g, '-').toLowerCase()) 198 | .replace('{Font+Family}', family.replace(/ /g, '+')) 199 | .replace('{fontweight}', weight) 200 | .replace( 201 | '{-fontstyle}', 202 | style.replace('normal', '').replace(/(.+)/, '-$1'), 203 | ) 204 | .replace('{fontstyle}', style); 205 | if (!document.querySelector('link[href="' + fontUrl + '"]')) { 206 | const markup = document.createElement('link'); 207 | markup.href = fontUrl; 208 | markup.rel = 'stylesheet'; 209 | document.head.appendChild(markup); 210 | } 211 | } 212 | } 213 | })(); 214 | 215 | return fonts; 216 | } 217 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import {VectorTile} from 'ol'; 2 | import TileState from 'ol/TileState.js'; 3 | import {toPromise} from 'ol/functions.js'; 4 | import {getUid} from 'ol/util.js'; 5 | import {normalizeSourceUrl, normalizeStyleUrl} from './mapbox.js'; 6 | 7 | /** @typedef {'Style'|'Source'|'Sprite'|'SpriteImage'|'Tiles'|'GeoJSON'} ResourceType */ 8 | 9 | /** @typedef {import("ol").Map} Map */ 10 | /** @typedef {import("ol/layer").Layer} Layer */ 11 | /** @typedef {import("ol/layer").Group} LayerGroup */ 12 | /** @typedef {import("ol/layer").Vector} VectorLayer */ 13 | /** @typedef {import("ol/layer").VectorTile} VectorTileLayer */ 14 | /** @typedef {import("ol/source").Source} Source */ 15 | 16 | export const emptyObj = Object.freeze({}); 17 | 18 | const functionCacheByStyleId = {}; 19 | const filterCacheByStyleId = {}; 20 | 21 | let styleId = 0; 22 | export function getStyleId(glStyle) { 23 | if (!glStyle.id) { 24 | glStyle.id = styleId++; 25 | } 26 | return glStyle.id; 27 | } 28 | 29 | export function getStyleFunctionKey(glStyle, olLayer) { 30 | return getStyleId(glStyle) + '.' + getUid(olLayer); 31 | } 32 | 33 | /** 34 | * @param {Object} glStyle Mapboox style object. 35 | * @return {Object} Function cache. 36 | */ 37 | export function getFunctionCache(glStyle) { 38 | let functionCache = functionCacheByStyleId[glStyle.id]; 39 | if (!functionCache) { 40 | functionCache = {}; 41 | functionCacheByStyleId[getStyleId(glStyle)] = functionCache; 42 | } 43 | return functionCache; 44 | } 45 | 46 | export function clearFunctionCache() { 47 | for (const key in functionCacheByStyleId) { 48 | delete functionCacheByStyleId[key]; 49 | } 50 | } 51 | 52 | /** 53 | * @param {Object} glStyle Mapboox style object. 54 | * @return {Object} Filter cache. 55 | */ 56 | export function getFilterCache(glStyle) { 57 | let filterCache = filterCacheByStyleId[glStyle.id]; 58 | if (!filterCache) { 59 | filterCache = {}; 60 | filterCacheByStyleId[getStyleId(glStyle)] = filterCache; 61 | } 62 | return filterCache; 63 | } 64 | 65 | export function deg2rad(degrees) { 66 | return (degrees * Math.PI) / 180; 67 | } 68 | 69 | export const defaultResolutions = (function () { 70 | const resolutions = []; 71 | for (let res = 78271.51696402048; resolutions.length <= 24; res /= 2) { 72 | resolutions.push(res); 73 | } 74 | return resolutions; 75 | })(); 76 | 77 | /** 78 | * @param {number} width Width of the canvas. 79 | * @param {number} height Height of the canvas. 80 | * @return {HTMLCanvasElement} Canvas. 81 | */ 82 | export function createCanvas(width, height) { 83 | if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope && typeof OffscreenCanvas !== 'undefined') { // eslint-disable-line 84 | return /** @type {?} */ (new OffscreenCanvas(width, height)); 85 | } 86 | const canvas = document.createElement('canvas'); 87 | canvas.width = width; 88 | canvas.height = height; 89 | return canvas; 90 | } 91 | 92 | export function getZoomForResolution(resolution, resolutions) { 93 | let i = 0; 94 | const ii = resolutions.length; 95 | for (; i < ii; ++i) { 96 | const candidate = resolutions[i]; 97 | if (candidate < resolution && i + 1 < ii) { 98 | const zoomFactor = resolutions[i] / resolutions[i + 1]; 99 | return i + Math.log(resolutions[i] / resolution) / Math.log(zoomFactor); 100 | } 101 | } 102 | return ii - 1; 103 | } 104 | 105 | export function getResolutionForZoom(zoom, resolutions) { 106 | const base = Math.floor(zoom); 107 | const factor = Math.pow(2, zoom - base); 108 | return resolutions[base] / factor; 109 | } 110 | 111 | const pendingRequests = {}; 112 | /** 113 | * @param {ResourceType} resourceType Type of resource to load. 114 | * @param {string} url Url of the resource. 115 | * @param {Options} [options] Options. 116 | * @param {{url?: string}} [metadata] Object to be filled with the request. 117 | * @return {Promise} Promise that resolves with the loaded resource 118 | * or rejects with the Response object. 119 | * @private 120 | */ 121 | export function fetchResource(resourceType, url, options = {}, metadata) { 122 | if (url in pendingRequests) { 123 | if (metadata) { 124 | metadata.url = pendingRequests[url][0].url; 125 | } 126 | return pendingRequests[url][1]; 127 | } 128 | const transformedRequest = options.transformRequest 129 | ? options.transformRequest(url, resourceType) || url 130 | : url; 131 | const handleError = function (error) { 132 | delete pendingRequests[url]; 133 | return Promise.reject(new Error('Error fetching source ' + url)); 134 | }; 135 | const handleResponse = function (response) { 136 | delete pendingRequests[url]; 137 | return response.ok 138 | ? response.json() 139 | : Promise.reject(new Error('Error fetching source ' + url)); 140 | }; 141 | const pendingRequest = toPromise(() => transformedRequest) 142 | .then((transformedRequest) => { 143 | if (transformedRequest instanceof Response) { 144 | if (metadata) { 145 | metadata.url = transformedRequest.url; 146 | } 147 | return handleResponse(transformedRequest); 148 | } 149 | if (!(transformedRequest instanceof Request)) { 150 | transformedRequest = new Request(transformedRequest); 151 | } 152 | if (!transformedRequest.headers.get('Accept')) { 153 | transformedRequest.headers.set('Accept', 'application/json'); 154 | } 155 | if (metadata) { 156 | metadata.url = transformedRequest.url; 157 | } 158 | return fetch(transformedRequest).then(handleResponse).catch(handleError); 159 | }) 160 | .catch(handleError); 161 | pendingRequests[url] = [transformedRequest, pendingRequest]; 162 | return pendingRequest; 163 | } 164 | 165 | export function getGlStyle(glStyleOrUrl, options) { 166 | if (typeof glStyleOrUrl === 'string') { 167 | if (glStyleOrUrl.trim().startsWith('{')) { 168 | try { 169 | const glStyle = JSON.parse(glStyleOrUrl); 170 | return Promise.resolve(glStyle); 171 | } catch (error) { 172 | return Promise.reject(error); 173 | } 174 | } else { 175 | glStyleOrUrl = normalizeStyleUrl(glStyleOrUrl, options.accessToken); 176 | return fetchResource('Style', glStyleOrUrl, options); 177 | } 178 | } else { 179 | return Promise.resolve(glStyleOrUrl); 180 | } 181 | } 182 | 183 | const tilejsonCache = {}; 184 | /** 185 | * @param {Object} glSource glStyle source object. 186 | * @param {string} styleUrl Style URL. 187 | * @param {Options} options Options. 188 | * @return {Promise<{tileJson: Object, tileLoadFunction: import('ol/Tile.js').LoadFunction}?>} TileJson and load function 189 | */ 190 | export function getTileJson(glSource, styleUrl, options = {}) { 191 | const cacheKey = [styleUrl, JSON.stringify(glSource)].toString(); 192 | let promise = tilejsonCache[cacheKey]; 193 | if (!promise || options.transformRequest) { 194 | let tileLoadFunction; 195 | if (options.transformRequest) { 196 | tileLoadFunction = (tile, src) => { 197 | const transformedRequest = options.transformRequest 198 | ? options.transformRequest(src, 'Tiles') || src 199 | : src; 200 | if (tile instanceof VectorTile) { 201 | tile.setLoader((extent, resolution, projection) => { 202 | const handleResponse = function (response) { 203 | response.arrayBuffer().then((data) => { 204 | const format = tile.getFormat(); 205 | const features = format.readFeatures(data, { 206 | extent: extent, 207 | featureProjection: projection, 208 | }); 209 | // @ts-ignore 210 | tile.setFeatures(features); 211 | }); 212 | }; 213 | toPromise(() => transformedRequest) 214 | .then((transformedRequest) => { 215 | if (transformedRequest instanceof Response) { 216 | return handleResponse(transformedRequest); 217 | } 218 | fetch(transformedRequest) 219 | .then(handleResponse) 220 | .catch((e) => tile.setState(TileState.ERROR)); 221 | }) 222 | .catch((e) => tile.setState(TileState.ERROR)); 223 | }); 224 | } else { 225 | const img = tile.getImage(); 226 | toPromise(() => transformedRequest) 227 | .then((transformedRequest) => { 228 | if (typeof transformedRequest === 'string') { 229 | img.src = transformedRequest; 230 | return; 231 | } 232 | const handleResponse = (response) => 233 | response.blob().then((blob) => { 234 | const url = URL.createObjectURL(blob); 235 | img.addEventListener('load', () => URL.revokeObjectURL(url)); 236 | img.addEventListener('error', () => URL.revokeObjectURL(url)); 237 | img.src = url; 238 | }); 239 | if (transformedRequest instanceof Response) { 240 | return handleResponse(transformedRequest); 241 | } 242 | fetch(transformedRequest) 243 | .then(handleResponse) 244 | .catch((e) => tile.setState(TileState.ERROR)); 245 | }) 246 | .catch((e) => tile.setState(TileState.ERROR)); 247 | } 248 | }; 249 | } 250 | const url = glSource.url; 251 | if (url && !glSource.tiles) { 252 | const normalizedSourceUrl = normalizeSourceUrl( 253 | url, 254 | options.accessToken, 255 | options.accessTokenParam || 'access_token', 256 | styleUrl || location.href, 257 | ); 258 | if (url.startsWith('mapbox://')) { 259 | promise = Promise.resolve({ 260 | tileJson: Object.assign({}, glSource, { 261 | url: undefined, 262 | tiles: normalizedSourceUrl, 263 | }), 264 | tileLoadFunction, 265 | }); 266 | } else { 267 | const metadata = {}; 268 | promise = fetchResource( 269 | 'Source', 270 | normalizedSourceUrl[0], 271 | options, 272 | metadata, 273 | ).then(function (tileJson) { 274 | tileJson.tiles = tileJson.tiles.map(function (tileUrl) { 275 | if (tileJson.scheme === 'tms') { 276 | tileUrl = tileUrl.replace('{y}', '{-y}'); 277 | } 278 | return normalizeSourceUrl( 279 | tileUrl, 280 | options.accessToken, 281 | options.accessTokenParam || 'access_token', 282 | metadata.url, 283 | )[0]; 284 | }); 285 | return Promise.resolve({tileJson, tileLoadFunction}); 286 | }); 287 | } 288 | } else if (glSource.tiles) { 289 | glSource = Object.assign({}, glSource, { 290 | tiles: glSource.tiles.map(function (tileUrl) { 291 | if (glSource.scheme === 'tms') { 292 | tileUrl = tileUrl.replace('{y}', '{-y}'); 293 | } 294 | return normalizeSourceUrl( 295 | tileUrl, 296 | options.accessToken, 297 | options.accessTokenParam || 'access_token', 298 | styleUrl || location.href, 299 | )[0]; 300 | }), 301 | }); 302 | promise = Promise.resolve({ 303 | tileJson: Object.assign({}, glSource), 304 | tileLoadFunction, 305 | }); 306 | } else { 307 | promise = Promise.reject(new Error('source has no `tiles` nor `url`')); 308 | } 309 | tilejsonCache[cacheKey] = promise; 310 | } 311 | return promise; 312 | } 313 | 314 | /** 315 | * @param {HTMLImageElement|HTMLCanvasElement} spriteImage Sprite image id. 316 | * @param {{x: number, y: number, width: number, height: number, pixelRatio: number}} spriteImageData Sprite image data. 317 | * @param {number} haloWidth Halo width. 318 | * @param {{r: number, g: number, b: number, a: number}} haloColor Halo color. 319 | * @return {HTMLCanvasElement} Canvas element with the halo. 320 | */ 321 | export function drawIconHalo( 322 | spriteImage, 323 | spriteImageData, 324 | haloWidth, 325 | haloColor, 326 | ) { 327 | const imgSize = [ 328 | 2 * haloWidth * spriteImageData.pixelRatio + spriteImageData.width, 329 | 2 * haloWidth * spriteImageData.pixelRatio + spriteImageData.height, 330 | ]; 331 | const imageCanvas = createCanvas(imgSize[0], imgSize[1]); 332 | const imageContext = imageCanvas.getContext('2d'); 333 | imageContext.drawImage( 334 | spriteImage, 335 | spriteImageData.x, 336 | spriteImageData.y, 337 | spriteImageData.width, 338 | spriteImageData.height, 339 | haloWidth * spriteImageData.pixelRatio, 340 | haloWidth * spriteImageData.pixelRatio, 341 | spriteImageData.width, 342 | spriteImageData.height, 343 | ); 344 | const imageData = imageContext.getImageData(0, 0, imgSize[0], imgSize[1]); 345 | imageContext.globalCompositeOperation = 'destination-over'; 346 | imageContext.fillStyle = `rgba(${haloColor.r * 255},${haloColor.g * 255},${ 347 | haloColor.b * 255 348 | },${haloColor.a})`; 349 | const data = imageData.data; 350 | for (let i = 0, ii = imageData.width; i < ii; ++i) { 351 | for (let j = 0, jj = imageData.height; j < jj; ++j) { 352 | const index = (j * ii + i) * 4; 353 | const alpha = data[index + 3]; 354 | if (alpha > 0) { 355 | imageContext.arc( 356 | i, 357 | j, 358 | haloWidth * spriteImageData.pixelRatio, 359 | 0, 360 | 2 * Math.PI, 361 | ); 362 | } 363 | } 364 | } 365 | imageContext.fill(); 366 | return imageCanvas; 367 | } 368 | 369 | function smoothstep(min, max, value) { 370 | const x = Math.max(0, Math.min(1, (value - min) / (max - min))); 371 | return x * x * (3 - 2 * x); 372 | } 373 | 374 | /** 375 | * @param {HTMLImageElement|HTMLCanvasElement} image SDF image 376 | * @param {{x: number, y: number, width: number, height: number}} area Area to unSDF 377 | * @param {{r: number, g: number, b: number, a: number}} color Color to use 378 | * @return {HTMLCanvasElement} Regular image 379 | */ 380 | export function drawSDF(image, area, color) { 381 | const imageCanvas = createCanvas(area.width, area.height); 382 | const imageContext = imageCanvas.getContext('2d'); 383 | imageContext.drawImage( 384 | image, 385 | area.x, 386 | area.y, 387 | area.width, 388 | area.height, 389 | 0, 390 | 0, 391 | area.width, 392 | area.height, 393 | ); 394 | const imageData = imageContext.getImageData(0, 0, area.width, area.height); 395 | const data = imageData.data; 396 | for (let i = 0, ii = imageData.width; i < ii; ++i) { 397 | for (let j = 0, jj = imageData.height; j < jj; ++j) { 398 | const index = (j * ii + i) * 4; 399 | const dist = data[index + 3] / 255; 400 | 401 | const buffer = 0.75; 402 | const gamma = 0.1; 403 | 404 | const alpha = smoothstep(buffer - gamma, buffer + gamma, dist); 405 | if (alpha > 0) { 406 | data[index + 0] = Math.round(255 * color.r * alpha); 407 | data[index + 1] = Math.round(255 * color.g * alpha); 408 | data[index + 2] = Math.round(255 * color.b * alpha); 409 | data[index + 3] = Math.round(255 * alpha); 410 | } else { 411 | data[index + 3] = 0; 412 | } 413 | } 414 | } 415 | imageContext.putImageData(imageData, 0, 0); 416 | return imageCanvas; 417 | } 418 | 419 | /** 420 | * @typedef {import("./apply.js").Options} Options 421 | * @private 422 | */ 423 | -------------------------------------------------------------------------------- /test/MapboxVectorLayer.test.js: -------------------------------------------------------------------------------- 1 | import Map from 'ol/Map.js'; 2 | import {unByKey} from 'ol/Observable.js'; 3 | import View from 'ol/View.js'; 4 | import should from 'should'; 5 | import MapboxVectorLayer from '../src/MapboxVectorLayer.js'; 6 | import {defaultResolutions, getZoomForResolution} from '../src/util.js'; 7 | 8 | describe('ol/layer/MapboxVector', () => { 9 | describe('TileJSON', function () { 10 | it('lets ol-mapbox-style handle TileJSON URLs', function (done) { 11 | const layer = new MapboxVectorLayer({ 12 | styleUrl: 13 | 'data:,' + 14 | encodeURIComponent( 15 | JSON.stringify({ 16 | version: 8, 17 | sources: { 18 | 'foo': { 19 | url: './fixtures/tilejson-mapboxvector.json', 20 | type: 'vector', 21 | }, 22 | }, 23 | layers: [ 24 | { 25 | 'id': 'mock', 26 | 'source': 'foo', 27 | }, 28 | ], 29 | }), 30 | ), 31 | }); 32 | layer.on('error', function (e) { 33 | done(e.error); 34 | }); 35 | const source = layer.getSource(); 36 | const key = source.on('change', function () { 37 | if (source.getState() === 'ready') { 38 | unByKey(key); 39 | should(source.getTileUrlFunction()([0, 0, 0])).eql( 40 | 'http://a.tiles.mapbox.com/v3/mapbox.geography-class/0/0/0.png', 41 | ); 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | it('chooses the correct tile source heuristically', function (done) { 48 | const layer = new MapboxVectorLayer({ 49 | styleUrl: 50 | 'data:,' + 51 | encodeURIComponent( 52 | JSON.stringify({ 53 | version: 8, 54 | sources: { 55 | 'foo': { 56 | type: 'vector', 57 | attribution: 'test-source', 58 | }, 59 | 'bar': { 60 | url: './fixtures/tilejson-mapboxvector.json', 61 | type: 'vector', 62 | }, 63 | }, 64 | layers: [ 65 | { 66 | id: 'mock', 67 | source: 'bar', 68 | }, 69 | ], 70 | }), 71 | ), 72 | }); 73 | layer.on('error', function (e) { 74 | done(e.error); 75 | }); 76 | const source = layer.getSource(); 77 | const key = source.on('change', function () { 78 | if (source.getState() === 'ready') { 79 | unByKey(key); 80 | should(source.getTileUrlFunction()([0, 0, 0])).eql( 81 | 'http://a.tiles.mapbox.com/v3/mapbox.geography-class/0/0/0.png', 82 | ); 83 | done(); 84 | } 85 | }); 86 | }); 87 | }); 88 | 89 | describe('maxResolution', function () { 90 | const styleUrl = 91 | 'data:,' + 92 | encodeURIComponent( 93 | JSON.stringify({ 94 | version: 8, 95 | sources: { 96 | 'foo': { 97 | tiles: ['/spec/ol/data/{z}-{x}-{y}.vector.pbf'], 98 | type: 'vector', 99 | tileSize: 256, 100 | minzoom: 6, 101 | }, 102 | }, 103 | layers: [ 104 | { 105 | id: 'mock', 106 | source: 'foo', 107 | }, 108 | ], 109 | }), 110 | ); 111 | 112 | it('accepts minZoom from configuration', function (done) { 113 | const layer = new MapboxVectorLayer({ 114 | minZoom: 5, 115 | styleUrl: styleUrl, 116 | }); 117 | const source = layer.getSource(); 118 | source.on('change', function onchange() { 119 | if (source.getState() === 'ready') { 120 | source.un('change', onchange); 121 | should(layer.getMaxResolution()).eql(Infinity); 122 | done(); 123 | } 124 | }); 125 | }); 126 | 127 | it('uses minZoom from source', function (done) { 128 | const layer = new MapboxVectorLayer({ 129 | styleUrl: styleUrl, 130 | }); 131 | layer.on('error', function (e) { 132 | done(e.error); 133 | }); 134 | const source = layer.getSource(); 135 | source.on('change', function onchange() { 136 | if (source.getState() === 'ready') { 137 | source.un('change', onchange); 138 | should( 139 | getZoomForResolution(layer.getMaxResolution(), defaultResolutions) + 140 | 1e-12, 141 | ).eql(5); 142 | done(); 143 | } 144 | }); 145 | }); 146 | }); 147 | 148 | describe('background', function () { 149 | let map; 150 | const mapDiv = document.createElement('div'); 151 | mapDiv.style.width = '20px'; 152 | mapDiv.style.height = '20px'; 153 | beforeEach(function () { 154 | document.body.appendChild(mapDiv); 155 | map = new Map({ 156 | target: mapDiv, 157 | view: new View({ 158 | zoom: 2, 159 | center: [0, 0], 160 | }), 161 | }); 162 | }); 163 | afterEach(function () { 164 | map.setTarget(null); 165 | document.body.removeChild(mapDiv); 166 | }); 167 | 168 | it('works for styles without background', function (done) { 169 | const layer = new MapboxVectorLayer({ 170 | styleUrl: 171 | 'data:,' + 172 | encodeURIComponent( 173 | JSON.stringify({ 174 | version: 8, 175 | sources: { 176 | 'foo': { 177 | tiles: ['/spec/ol/data/{z}-{x}-{y}.vector.pbf'], 178 | type: 'vector', 179 | }, 180 | }, 181 | layers: [ 182 | { 183 | id: 'landuse', 184 | type: 'fill', 185 | source: 'foo', 186 | 'source-layer': 'landuse', 187 | paint: { 188 | 'fill-color': '#ff0000', 189 | 'fill-opacity': 0.8, 190 | }, 191 | }, 192 | ], 193 | }), 194 | ), 195 | }); 196 | map.addLayer(layer); 197 | layer.getSource().once('change', () => { 198 | layer.once('postrender', (e) => { 199 | const pixel = Array.from(e.context.getImageData(0, 0, 1, 1).data); 200 | should(pixel).eql([0, 0, 0, 0]); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | describe('Access token', function () { 208 | let originalFetch, fetchUrl; 209 | beforeEach(function () { 210 | originalFetch = fetch; 211 | window.fetch = function (url) { 212 | fetchUrl = url; 213 | return Promise.resolve({ok: false}); 214 | }; 215 | }); 216 | afterEach(function () { 217 | window.fetch = originalFetch; 218 | }); 219 | it('applies correct access token', function (done) { 220 | new MapboxVectorLayer({ 221 | styleUrl: 'mapbox://styles/mapbox/streets-v7', 222 | accessToken: '123', 223 | }) 224 | .getSource() 225 | .once('change', () => { 226 | should(fetchUrl.url).eql( 227 | 'https://api.mapbox.com/styles/v1/mapbox/streets-v7?&access_token=123', 228 | ); 229 | done(); 230 | }); 231 | }); 232 | it('applies correct access token from url', function (done) { 233 | new MapboxVectorLayer({ 234 | styleUrl: 'foo?key=123', 235 | }) 236 | .getSource() 237 | .once('change', () => { 238 | should(fetchUrl.url).eql(`${location.origin}/foo?key=123`); 239 | done(); 240 | }); 241 | }); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /test/applyStyle.test.js: -------------------------------------------------------------------------------- 1 | import ImageLayer from 'ol/layer/Image.js'; 2 | import VectorLayer from 'ol/layer/Vector.js'; 3 | import VectorTileLayer from 'ol/layer/VectorTile.js'; 4 | import {get} from 'ol/proj.js'; 5 | import VectorSource from 'ol/source/Vector.js'; 6 | import VectorTileSource from 'ol/source/VectorTile.js'; 7 | import {createXYZ} from 'ol/tilegrid.js'; 8 | import should from 'should'; 9 | import {spy} from 'sinon'; 10 | 11 | import {apply, applyStyle} from '../src/apply.js'; 12 | import {defaultResolutions, getZoomForResolution} from '../src/util.js'; 13 | import glStyle from './fixtures/osm-liberty/style.json'; 14 | import styleEmptySprite from './fixtures/style-empty-sprite.json'; 15 | import styleInvalidSpriteURL from './fixtures/style-invalid-sprite-url.json'; 16 | import styleInvalidVersion from './fixtures/style-invalid-version.json'; 17 | import styleMissingSprite from './fixtures/style-missing-sprite.json'; 18 | 19 | describe('applyStyle with source creation', function () { 20 | let originalFetch; 21 | const requests = []; 22 | beforeEach(function () { 23 | originalFetch = fetch; 24 | window.fetch = (request) => { 25 | requests.push(request); 26 | return originalFetch(request); 27 | }; 28 | }); 29 | afterEach(function () { 30 | window.fetch = originalFetch; 31 | requests.length = 0; 32 | }); 33 | 34 | it('accepts incorrect source with simple 3-parameter configuration', function (done) { 35 | const layer = new VectorLayer(); 36 | applyStyle(layer, '/fixtures/geojson.json', { 37 | source: 'not_in_style', 38 | }) 39 | .then(() => { 40 | done(new Error('Expected to reject')); 41 | }) 42 | .catch((e) => done()); 43 | }); 44 | it('accepts correct source with simple 3-parameter configuration', function (done) { 45 | const layer = new VectorLayer(); 46 | applyStyle(layer, '/fixtures/geojson.json', { 47 | source: 'states', 48 | }) 49 | .then(() => { 50 | done(); 51 | }) 52 | .catch((e) => done(e)); 53 | }); 54 | it('configures vector layer with source and style', function (done) { 55 | const layer = new VectorLayer(); 56 | applyStyle(layer, '/fixtures/geojson.json').then(function () { 57 | try { 58 | should(layer.getSource()).be.an.instanceOf(VectorSource); 59 | should(layer.getSource().getUrl()).equal( 60 | `${location.origin}/fixtures/states.geojson`, 61 | ); 62 | should(layer.getStyle()).be.an.instanceOf(Function); 63 | done(); 64 | } catch (e) { 65 | done(e); 66 | } 67 | }); 68 | }); 69 | it('uses a Request object from the transformRequest option', function (done) { 70 | const layer = new VectorLayer(); 71 | applyStyle(layer, '/fixtures/geojson.json', 'states', { 72 | transformRequest: function (url, type) { 73 | if (type === 'GeoJSON') { 74 | url += '?foo=bar'; 75 | } 76 | return new Request(url); 77 | }, 78 | }).then(function () { 79 | layer 80 | .getSource() 81 | .loadFeatures([0, 0, 10000, 10000], 10, get('EPSG:3857')); 82 | layer.getSource().once('change', () => { 83 | try { 84 | should(layer.getSource()).be.an.instanceOf(VectorSource); 85 | should(requests[requests.length - 1].url).equal( 86 | `${location.origin}/fixtures/states.geojson?foo=bar`, 87 | ); 88 | should(layer.getStyle()).be.an.instanceOf(Function); 89 | done(); 90 | } catch (e) { 91 | done(e); 92 | } 93 | }); 94 | }); 95 | }); 96 | it('respects source options from layer config', function (done) { 97 | const source = new VectorSource(); 98 | const layer = new VectorLayer({ 99 | source: source, 100 | }); 101 | const loader = function (extent, resolution, projection, success, failure) { 102 | fetch(/** @type {string} */ (layer.getSource().getUrl())) 103 | .then((response) => { 104 | response.json().then((json) => { 105 | const features = this.getFormat().readFeatures(json, { 106 | featureProjection: projection, 107 | }); 108 | success( 109 | /** @type {Array} */ (features), 110 | ); 111 | }); 112 | }) 113 | .catch((error) => { 114 | failure(); 115 | }); 116 | }; 117 | layer.getSource().setLoader(loader); 118 | 119 | applyStyle(layer, '/fixtures/geojson.json', 'states').then(function () { 120 | try { 121 | should(layer.getSource()).equal(source); 122 | should(layer.getSource().getUrl()).equal( 123 | `${location.origin}/fixtures/states.geojson`, 124 | ); 125 | should(layer.getSource().loader_).equal(loader); 126 | should(layer.getStyle()).be.an.instanceOf(Function); 127 | done(); 128 | } catch (e) { 129 | done(e); 130 | } 131 | }); 132 | }); 133 | it('configures vector tile layer with source and style', function (done) { 134 | const layer = new VectorTileLayer(); 135 | applyStyle(layer, '/fixtures/osm-liberty/style.json') 136 | .then(function () { 137 | try { 138 | should(layer.getSource()).be.an.instanceOf(VectorTileSource); 139 | should(layer.getSource().getUrls()[0]).equal( 140 | `${location.origin}/fixtures/osm-liberty/tiles/v3/{z}/{x}/{y}.pbf`, 141 | ); 142 | should(layer.getStyle()).be.an.instanceOf(Function); 143 | should(layer.get('mapbox-source')).equal('openmaptiles'); 144 | should(layer.get('mapbox-layers').length).equal(94); 145 | done(); 146 | } catch (e) { 147 | done(e); 148 | } 149 | }) 150 | .catch(function (e) { 151 | done(e); 152 | }); 153 | }); 154 | it('respects the transformRequest option', function (done) { 155 | const layer = new VectorTileLayer(); 156 | applyStyle(layer, '/fixtures/osm-liberty/style.json', 'openmaptiles', { 157 | transformRequest(url, type) { 158 | if (type === 'Tiles') { 159 | url += '?foo=bar'; 160 | } 161 | return url; 162 | }, 163 | }) 164 | .then(function () { 165 | try { 166 | should(layer.getSource()).be.an.instanceOf(VectorTileSource); 167 | const image = {}; 168 | Object.defineProperty(image, 'src', { 169 | set: function (src) { 170 | should(src).equal( 171 | `${location.origin}/fixtures/osm-liberty/tiles/v3/0/0/0.pbf?foo=bar`, 172 | ); 173 | should(layer.getStyle()).be.an.instanceOf(Function); 174 | done(); 175 | }, 176 | }); 177 | const img = {getImage: () => image}; 178 | layer.getSource().getTileLoadFunction()( 179 | img, 180 | `${location.origin}/fixtures/osm-liberty/tiles/v3/0/0/0.pbf`, 181 | ); 182 | } catch (e) { 183 | done(e); 184 | } 185 | }) 186 | .catch(function (e) { 187 | done(e); 188 | }); 189 | }); 190 | it('respects source options from layer config', function (done) { 191 | const source = new VectorTileSource({}); 192 | const layer = new VectorTileLayer({ 193 | source: source, 194 | }); 195 | const loader = function (tile, url) { 196 | tile.setLoader(function (extent, resolution, projection) { 197 | fetch(url + '?foo=bar').then(function (response) { 198 | response.arrayBuffer().then(function (data) { 199 | const format = tile.getFormat(); 200 | const features = format.readFeatures(data, { 201 | extent: extent, 202 | featureProjection: projection, 203 | }); 204 | tile.setFeatures(features); 205 | }); 206 | }); 207 | }); 208 | }; 209 | layer.getSource().setTileLoadFunction(loader); 210 | applyStyle(layer, '/fixtures/osm-liberty/style.json', 'openmaptiles') 211 | .then(function () { 212 | try { 213 | should(layer.getSource()).equal(source); 214 | should(layer.getSource().getTileLoadFunction()).equal(loader); 215 | should(layer.getStyle()).be.an.instanceOf(Function); 216 | done(); 217 | } catch (e) { 218 | done(e); 219 | } 220 | }) 221 | .catch(function (e) { 222 | done(e); 223 | }); 224 | }); 225 | it('accepts options as 3rd argument', function (done) { 226 | const accessToken = 'mytoken'; 227 | const mapboxLayer = new VectorLayer(); 228 | applyStyle(mapboxLayer, 'mapbox://styles/my/style', { 229 | accessToken: accessToken, 230 | transformRequest(url, type) { 231 | should(url).endWith(accessToken); 232 | done(); 233 | }, 234 | }).catch(done); 235 | }); 236 | }); 237 | 238 | describe('applyStyle without source creation', function () { 239 | it('leaves vector source untouched when updateSource is false', function (done) { 240 | const source = new VectorSource({}); 241 | const layer = new VectorLayer({ 242 | source: source, 243 | }); 244 | const loader = function (extent, resolution, projection, success, failure) { 245 | fetch(/** @type {string} */ (layer.getSource().getUrl())) 246 | .then((response) => { 247 | response.json().then((json) => { 248 | const features = this.getFormat().readFeatures(json, { 249 | featureProjection: projection, 250 | }); 251 | success( 252 | /** @type {Array} */ (features), 253 | ); 254 | }); 255 | }) 256 | .catch((error) => { 257 | failure(); 258 | }); 259 | }; 260 | layer.getSource().setLoader(loader); 261 | applyStyle(layer, '/fixtures/osm-liberty/style.json', 'openmaptiles', { 262 | updateSource: false, 263 | }) 264 | .then(function () { 265 | try { 266 | should(layer.getSource()).equal(source); 267 | should(layer.getSource().getUrl()).be.undefined(); 268 | should(layer.getSource().getAttributions()).be.null(); 269 | should(layer.getSource().loader_).equal(loader); 270 | should(layer.getStyle()).be.an.instanceOf(Function); 271 | done(); 272 | } catch (e) { 273 | done(e); 274 | } 275 | }) 276 | .catch(function (e) { 277 | done(e); 278 | }); 279 | }); 280 | it('leaves vector tile source untouched when updateSource is false', function (done) { 281 | const source = new VectorTileSource({}); 282 | const layer = new VectorTileLayer({ 283 | source: source, 284 | }); 285 | const loader = function (tile, url) { 286 | tile.setLoader(function (extent, resolution, projection) { 287 | fetch(url + '?foo=bar').then(function (response) { 288 | response.arrayBuffer().then(function (data) { 289 | const format = tile.getFormat(); 290 | const features = format.readFeatures(data, { 291 | extent: extent, 292 | featureProjection: projection, 293 | }); 294 | tile.setFeatures(features); 295 | }); 296 | }); 297 | }); 298 | }; 299 | layer.getSource().setTileLoadFunction(loader); 300 | applyStyle(layer, '/fixtures/osm-liberty/style.json', 'openmaptiles', { 301 | updateSource: false, 302 | }) 303 | .then(function () { 304 | try { 305 | should(layer.getSource()).equal(source); 306 | should(layer.getSource().getUrls()).be.null(); 307 | should(layer.getSource().getAttributions()).be.null(); 308 | should(layer.getSource().getTileLoadFunction()).equal(loader); 309 | should(layer.getStyle()).be.an.instanceOf(Function); 310 | done(); 311 | } catch (e) { 312 | done(e); 313 | } 314 | }) 315 | .catch(function (e) { 316 | done(e); 317 | }); 318 | }); 319 | }); 320 | 321 | describe('maxResolution', function () { 322 | let glStyle; 323 | beforeEach(function () { 324 | glStyle = { 325 | version: 8, 326 | sources: { 327 | 'foo': { 328 | tiles: ['/fixtures/{z}-{x}-{y}.vector.pbf'], 329 | type: 'vector', 330 | minzoom: 6, 331 | }, 332 | }, 333 | layers: [ 334 | { 335 | id: 'mock', 336 | source: 'foo', 337 | }, 338 | ], 339 | }; 340 | }); 341 | 342 | it('accepts minZoom from configuration', function (done) { 343 | const layer = new VectorTileLayer({ 344 | minZoom: 5, 345 | }); 346 | applyStyle(layer, glStyle) 347 | .then(function () { 348 | should(layer.getMaxResolution()).equal(Infinity); 349 | done(); 350 | }) 351 | .catch(function (e) { 352 | done(e); 353 | }); 354 | }); 355 | 356 | it('uses minZoom from source', function (done) { 357 | const layer = new VectorTileLayer(); 358 | applyStyle(layer, glStyle) 359 | .then(function () { 360 | should( 361 | getZoomForResolution(layer.getMaxResolution(), defaultResolutions) + 362 | 1e-12, 363 | ).eql(6); 364 | done(); 365 | }) 366 | .catch(function (e) { 367 | done(e); 368 | }); 369 | }); 370 | 371 | it('but not when tilegrid starts at zoom 0', function (done) { 372 | glStyle.sources.foo.minzoom = 0; 373 | const layer = new VectorTileLayer(); 374 | applyStyle(layer, glStyle) 375 | .then(function () { 376 | should(layer.getMaxResolution()).equal(Infinity); 377 | done(); 378 | }) 379 | .catch(function (e) { 380 | done(e); 381 | }); 382 | }); 383 | }); 384 | 385 | describe('applyStyle style argument validation', function () { 386 | const source = 'openmaptiles'; 387 | const layer = new VectorTileLayer(); 388 | 389 | it('should handle valid style as JSON', function (done) { 390 | applyStyle(layer, glStyle, source, 'fixtures/osm-liberty/') 391 | .then(done) 392 | .catch(done); 393 | }); 394 | 395 | it('should handle valid style as JSON string', function (done) { 396 | applyStyle(layer, JSON.stringify(glStyle), source, 'fixtures/osm-liberty/') 397 | .then(done) 398 | .catch(done); 399 | }); 400 | 401 | it('should reject invalid style version', function (done) { 402 | applyStyle(layer, styleInvalidVersion, source, 'fixtures/osm-liberty/') 403 | .then(function () { 404 | done(new Error('invalid style version promise should reject')); 405 | }) 406 | .catch(function (err) { 407 | done(); 408 | }); 409 | }); 410 | 411 | it('should reject invalid ol layer type', function (done) { 412 | applyStyle(new ImageLayer(), glStyle, source, 'fixtures/osm-liberty/') 413 | .then(function () { 414 | done(new Error('invalid ol layer type promise should reject')); 415 | }) 416 | .catch(function (err) { 417 | done(); 418 | }); 419 | }); 420 | 421 | it('should reject invalid ol layer source type', function (done) { 422 | applyStyle( 423 | layer, 424 | glStyle, 425 | 'natural_earth_shaded_relief', 426 | 'fixtures/osm-liberty/', 427 | ) 428 | .then(function () { 429 | done(new Error('invalid ol layer source promise should reject')); 430 | }) 431 | .catch(function (err) { 432 | done(); 433 | }); 434 | }); 435 | }); 436 | 437 | describe('applyStyle style validation', function () { 438 | const source = 'openmaptiles'; 439 | const layer = new VectorTileLayer(); 440 | 441 | it('should handle missing sprite', function (done) { 442 | applyStyle(layer, styleMissingSprite, source, 'fixtures/osm-liberty/') 443 | .then(done) 444 | .catch(done); 445 | }); 446 | 447 | it('should handle empty sprite', function (done) { 448 | applyStyle(layer, styleEmptySprite, source, 'fixtures/osm-liberty/') 449 | .then(done) 450 | .catch(done); 451 | }); 452 | 453 | it('should reject invalid sprite URL', function (done) { 454 | applyStyle(layer, styleInvalidSpriteURL, source, 'fixtures/osm-liberty/') 455 | .then(function () { 456 | done(new Error('invalid sprite URL promise should reject')); 457 | }) 458 | .catch(function (err) { 459 | done(); 460 | }); 461 | }); 462 | }); 463 | 464 | describe('applyStyle sprite retrieval', function () { 465 | const source = 'openmaptiles'; 466 | const layer = new VectorTileLayer(); 467 | 468 | let origDevicePixelRatio, fetchSpy; 469 | beforeEach(function () { 470 | origDevicePixelRatio = self.devicePixelRatio; 471 | fetchSpy = spy(self, 'fetch'); 472 | }); 473 | 474 | afterEach(function () { 475 | devicePixelRatio = origDevicePixelRatio; 476 | self.fetch.restore(); 477 | }); 478 | 479 | it('should retrieve hires sprite', function (done) { 480 | devicePixelRatio = 2; 481 | applyStyle(layer, glStyle, source, 'fixtures/osm-liberty/') 482 | .then(function () { 483 | should(fetchSpy.getCall(0).args[0].url).endWith('/osm-liberty@2x.json'); 484 | should(fetchSpy.callCount).be.exactly(1); 485 | done(); 486 | }) 487 | .catch(function (error) { 488 | done(error); 489 | }); 490 | }); 491 | 492 | it('should retrieve lores sprite', function (done) { 493 | devicePixelRatio = 1; 494 | applyStyle(layer, glStyle, source, 'fixtures/osm-liberty/') 495 | .then(function () { 496 | should(fetchSpy.getCall(0).args[0].url).endWith('/osm-liberty.json'); 497 | should(fetchSpy.callCount).be.exactly(1); 498 | done(); 499 | }) 500 | .catch(function (error) { 501 | done(error); 502 | }); 503 | }); 504 | 505 | it('should fall through to lores when hires not available and reject on lores not available', function (done) { 506 | const style = Object.assign({}, glStyle); 507 | style.sprite = 508 | window.location.protocol + '//' + window.location.host + '/invalid'; 509 | 510 | devicePixelRatio = 2; 511 | 512 | applyStyle(layer, style, source) 513 | .then(function () { 514 | done(new Error('should not resolve')); 515 | }) 516 | .catch(function (error) { 517 | should(error.message.indexOf('/invalid.json')).be.greaterThan(-1); 518 | done(); 519 | }); 520 | }); 521 | 522 | it('should reject when sprite JSON is not found', function (done) { 523 | const style = Object.assign({}, glStyle); 524 | style.sprite = './not-found'; 525 | 526 | devicePixelRatio = 1; 527 | 528 | applyStyle(layer, style, source) 529 | .then(function () { 530 | done(new Error('sprite JSON not found - promise should reject')); 531 | }) 532 | .catch(function (err) { 533 | done(); 534 | }); 535 | }); 536 | }); 537 | 538 | describe('applyStyle functionality', function () { 539 | it('applies a style function to a layer and resolves promise', function (done) { 540 | const layer = new VectorTileLayer({ 541 | source: new VectorTileSource({ 542 | tileGrid: createXYZ({tileSize: 512, maxZoom: 22}), 543 | }), 544 | }); 545 | should(layer.getStyle()).be.null; 546 | applyStyle(layer, glStyle, 'openmaptiles', 'fixtures/osm-liberty/').then( 547 | function () { 548 | should(layer.getStyle()).be.a.Function(); 549 | done(); 550 | }, 551 | ); 552 | }); 553 | }); 554 | 555 | describe('applyStyle supports transformRequest object', function () { 556 | it('applies transformRequest to all Vector Tile request types', function (done) { 557 | const expectedRequestTypes = new Set([ 558 | 'Style', 559 | 'Sprite', 560 | 'SpriteImage', 561 | 'Source', 562 | 'Tiles', 563 | ]); 564 | const seenRequestTypes = new Set(); 565 | apply(document.createElement('div'), '/fixtures/hot-osm/hot-osm.json', { 566 | transformRequest: function (url, type) { 567 | seenRequestTypes.add(type); 568 | return new Request(url); 569 | }, 570 | }) 571 | .then(function (map) { 572 | map.once('rendercomplete', () => { 573 | should.deepEqual( 574 | expectedRequestTypes, 575 | seenRequestTypes, 576 | `Request types seen by transformRequest: ${Array.from( 577 | seenRequestTypes, 578 | )} do not match those expected for a Vector Tile style: ${Array.from( 579 | expectedRequestTypes, 580 | )}`, 581 | ); 582 | done(); 583 | }); 584 | map.setSize([100, 100]); 585 | }) 586 | .catch(function (error) { 587 | done(error); 588 | }); 589 | }); 590 | it('applies async transformRequest to all Vector Tile request types', function (done) { 591 | const expectedRequestTypes = new Set([ 592 | 'Style', 593 | 'Sprite', 594 | 'SpriteImage', 595 | 'Source', 596 | 'Tiles', 597 | ]); 598 | const seenRequestTypes = new Set(); 599 | apply(document.createElement('div'), '/fixtures/hot-osm/hot-osm.json', { 600 | transformRequest: function (url, type) { 601 | seenRequestTypes.add(type); 602 | return Promise.resolve(new Request(url)); 603 | }, 604 | }) 605 | .then(function (map) { 606 | map.once('rendercomplete', () => { 607 | should.deepEqual( 608 | expectedRequestTypes, 609 | seenRequestTypes, 610 | `Request types seen by transformRequest: ${Array.from( 611 | seenRequestTypes, 612 | )} do not match those expected for a Vector Tile style: ${Array.from( 613 | expectedRequestTypes, 614 | )}`, 615 | ); 616 | done(); 617 | }); 618 | map.setSize([100, 100]); 619 | }) 620 | .catch(function (error) { 621 | done(error); 622 | }); 623 | }); 624 | it('applies transformRequest to GeoJSON request types', function (done) { 625 | const expectedRequestTypes = new Set(['Style', 'GeoJSON']); 626 | const seenRequestTypes = new Set(); 627 | apply(document.createElement('div'), '/fixtures/geojson.json', { 628 | transformRequest: function (url, type) { 629 | seenRequestTypes.add(type); 630 | return new Request(url); 631 | }, 632 | }) 633 | .then(function (map) { 634 | map.once('rendercomplete', () => { 635 | should.deepEqual( 636 | expectedRequestTypes, 637 | seenRequestTypes, 638 | `Request types seen by transformRequest: ${Array.from( 639 | seenRequestTypes, 640 | )} do not match those expected for a GeoJSON style: ${Array.from( 641 | expectedRequestTypes, 642 | )}`, 643 | ); 644 | done(); 645 | }); 646 | map.setSize([100, 100]); 647 | }) 648 | .catch(function (error) { 649 | done(error); 650 | }); 651 | }); 652 | it('applies async transformRequest to GeoJSON request types', function (done) { 653 | const expectedRequestTypes = new Set(['Style', 'GeoJSON']); 654 | const seenRequestTypes = new Set(); 655 | apply(document.createElement('div'), '/fixtures/geojson.json', { 656 | transformRequest: function (url, type) { 657 | seenRequestTypes.add(type); 658 | return Promise.resolve(url); 659 | }, 660 | }) 661 | .then(function (map) { 662 | map.once('rendercomplete', () => { 663 | should.deepEqual( 664 | expectedRequestTypes, 665 | seenRequestTypes, 666 | `Request types seen by transformRequest: ${Array.from( 667 | seenRequestTypes, 668 | )} do not match those expected for a GeoJSON style: ${Array.from( 669 | expectedRequestTypes, 670 | )}`, 671 | ); 672 | done(); 673 | }); 674 | map.setSize([100, 100]); 675 | }) 676 | .catch(function (error) { 677 | done(error); 678 | }); 679 | }); 680 | }); 681 | -------------------------------------------------------------------------------- /test/finalizeLayer.test.js: -------------------------------------------------------------------------------- 1 | import Map from 'ol/Map.js'; 2 | import TileLayer from 'ol/layer/Tile.js'; 3 | import VectorTileLayer from 'ol/layer/VectorTile.js'; 4 | import TileSource from 'ol/source/Tile.js'; 5 | import VectorTileSource from 'ol/source/VectorTile.js'; 6 | 7 | import should from 'should'; 8 | import {finalizeLayer} from '../src/apply.js'; 9 | import glStyle from './fixtures/osm-liberty/style.json'; 10 | import invalidStyle from './fixtures/style-invalid-version.json'; 11 | 12 | describe('finalizeLayer promise', function () { 13 | it('should resolve with valid input and vector layer source', function (done) { 14 | const layer = new VectorTileLayer({ 15 | source: new VectorTileSource({}), 16 | }); 17 | const map = new Map(); 18 | 19 | finalizeLayer(layer, ['park'], glStyle, 'fixtures/osm-liberty/', map) 20 | .then(done) 21 | .catch(function (err) { 22 | done(err); 23 | }); 24 | }); 25 | 26 | it('should resolve with valid input and non-vector source', function (done) { 27 | const layer = new TileLayer({ 28 | source: new TileSource({}), 29 | }); 30 | const map = new Map({layers: [layer]}); 31 | 32 | finalizeLayer( 33 | layer, 34 | ['natural_earth'], 35 | glStyle, 36 | 'fixtures/osm-liberty/', 37 | map, 38 | ) 39 | .then(done) 40 | .catch(function (err) { 41 | done(err); 42 | }); 43 | }); 44 | 45 | it('should not resolve at all if layer source does not exist', function (done) { 46 | const layer = new VectorTileLayer(); 47 | let resolved = false; 48 | finalizeLayer(layer, ['eh'], glStyle, 'fixtures/osm-liberty/', new Map()) 49 | .then(function () { 50 | resolved = true; 51 | }) 52 | .catch(function (err) { 53 | done(err); 54 | }); 55 | 56 | setTimeout(function () { 57 | should(resolved).be.false; 58 | done(); 59 | }, 500); 60 | }); 61 | 62 | it('should not resolve until layer has a source', function (done) { 63 | const map = new Map(); 64 | const layer = new VectorTileLayer(); 65 | let resolved = false; 66 | let waitForSource = true; 67 | finalizeLayer(layer, ['park'], glStyle, 'fixtures/osm-liberty/', map) 68 | .then(function () { 69 | resolved = true; 70 | should(waitForSource).be.false; 71 | done(); 72 | }) 73 | .catch(function (err) { 74 | done(err); 75 | }); 76 | 77 | setTimeout(function () { 78 | waitForSource = false; 79 | should(resolved).be.false; 80 | layer.setSource(new VectorTileSource({})); 81 | }, 500); 82 | }); 83 | 84 | it('should reject if applyStyle fails', function (done) { 85 | // applyStyle will fail if glStyle's version prop is not '8' 86 | // note that to get to that point, the map has to have a layer that 87 | // has a source, as well as having stuff in layerIds. 88 | const layer = new VectorTileLayer({ 89 | source: new VectorTileSource({}), 90 | }); 91 | const map = new Map({layers: [layer]}); 92 | 93 | finalizeLayer(layer, ['eh'], invalidStyle, null, map) 94 | .then(function () { 95 | done(new Error('should not have succeeded')); 96 | }) 97 | .catch(function (err) { 98 | done(); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/fixtures/background-none.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "background", 4 | "metadata": { 5 | "mapbox:autocomposite": true, 6 | "mapbox:type": "template" 7 | }, 8 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 9 | "sources": {}, 10 | "layers": [ 11 | { 12 | "id": "background", 13 | "type": "background", 14 | "paint": { 15 | "background-color": "#f8f4f0", 16 | "background-opacity": 0.75 17 | }, 18 | "layout": { 19 | "visibility": "none" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/background.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "background", 4 | "metadata": { 5 | "mapbox:autocomposite": true, 6 | "mapbox:type": "template" 7 | }, 8 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 9 | "sources": {}, 10 | "layers": [ 11 | { 12 | "id": "background", 13 | "type": "background", 14 | "paint": { 15 | "background-color": "#f8f4f0", 16 | "background-opacity": 0.75 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/geography-class.json: -------------------------------------------------------------------------------- 1 | { 2 | "attribution": "", 3 | "bounds": [ 4 | -180, 5 | -85.05112877980659, 6 | 180, 7 | 85.05112877980659 8 | ], 9 | "center": [ 10 | 0, 11 | 0, 12 | 4 13 | ], 14 | "created": 1345755770000, 15 | "description": "One of the example maps that comes with TileMill - a bright & colorful world map that blends retro and high-tech with its folded paper texture and interactive flag tooltips. ", 16 | "download": "http://a.tiles.mapbox.com/v3/aj.1x1-degrees.mbtiles", 17 | "embed": "http://a.tiles.mapbox.com/v3/aj.1x1-degrees.html", 18 | "filesize": 134727680, 19 | "grids": [ 20 | "http://a.tiles.mapbox.com/v3/aj.1x1-degrees/{z}/{x}/{y}.grid.json", 21 | "http://b.tiles.mapbox.com/v3/aj.1x1-degrees/{z}/{x}/{y}.grid.json" 22 | ], 23 | "id": "aj.1x1-degrees", 24 | "legend": "
\n\n
Geography Class
\n
by MapBox
\n\n\n
", 25 | "mapbox_logo": true, 26 | "maxzoom": 8, 27 | "minzoom": 0, 28 | "name": "Geography Class", 29 | "private": false, 30 | "scheme": "xyz", 31 | "template": "{{#__location__}}{{/__location__}}{{#__teaser__}}
\n\n
\n{{admin}}\n\n
{{/__teaser__}}{{#__full__}}{{/__full__}}", 32 | "tilejson": "2.2.0", 33 | "tiles": [ 34 | "http://a.tiles.mapbox.com/v3/aj.1x1-degrees/{z}/{x}/{y}.png", 35 | "http://b.tiles.mapbox.com/v3/aj.1x1-degrees/{z}/{x}/{y}.png" 36 | ], 37 | "version": "1.0.0", 38 | "webpage": "http://a.tiles.mapbox.com/v3/aj.1x1-degrees/page.html" 39 | } -------------------------------------------------------------------------------- /test/fixtures/geojson-wfs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "wfs", 4 | "center": [-79.882797, 43.513489], 5 | "zoom": 11, 6 | "glyphs": "{fontstack}/{range}", 7 | "sources": { 8 | "water_areas": { 9 | "type": "geojson", 10 | "data": "?service=WFS&version=1.1.0&request=GetFeature&typename=osm:water_areas&outputFormat=application/json&srsname=EPSG:4326&bbox={bbox-epsg-3857}" 11 | } 12 | }, 13 | "layers": [ 14 | { 15 | "id": "background", 16 | "type": "background", 17 | "paint": { 18 | "background-color": "rgba(255,255,0,0.2)" 19 | } 20 | }, 21 | { 22 | "id": "water_areas_fill", 23 | "type": "fill", 24 | "source": "water_areas", 25 | "paint": { 26 | "fill-color": "#020E5D", 27 | "fill-opacity": 0.8 28 | }, 29 | "minzoom": 5 30 | }, 31 | { 32 | "id": "water_areas_line", 33 | "type": "line", 34 | "source": "water_areas", 35 | "paint": { 36 | "fill-color": "white" 37 | }, 38 | "minzoom": 6 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/geojson.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states", 4 | "center": [-122.19952899999998, 51.920367528011525], 5 | "zoom": 3, 6 | "glyphs": "{fontstack}/{range}", 7 | "sources": { 8 | "states": { 9 | "type": "geojson", 10 | "data": "./states.geojson" 11 | } 12 | }, 13 | "layers": [ 14 | { 15 | "id": "background", 16 | "type": "background", 17 | "paint": { 18 | "background-color": "rgba(0,0,0,0)" 19 | } 20 | }, 21 | { 22 | "id": "population_lt_2m", 23 | "type": "fill", 24 | "source": "states", 25 | "filter": ["<=", "PERSONS", 2000000], 26 | "paint": { 27 | "fill-color": "#A6CEE3", 28 | "fill-opacity": 0.7 29 | } 30 | }, 31 | { 32 | "id": "2m_lt_population_lte_4m", 33 | "type": "fill", 34 | "source": "states", 35 | "filter": ["all", [">", "PERSONS", 2000000], ["<=", "PERSONS", 4000000]], 36 | "paint": { 37 | "fill-color": "#0F78B4", 38 | "fill-opacity": 0.7 39 | } 40 | }, 41 | { 42 | "id": "population_gt_4m", 43 | "type": "fill", 44 | "source": "states", 45 | "filter": [">", "PERSONS", 4000000], 46 | "paint": { 47 | "fill-color": "#B2DF8A", 48 | "fill-opacity": 0.7 49 | } 50 | }, 51 | { 52 | "id": "state_outlines", 53 | "type": "line", 54 | "source": "states", 55 | "paint": { 56 | "line-color": "#8cadbf", 57 | "line-width": 0.1 58 | } 59 | }, 60 | { 61 | "id": "state_abbreviations", 62 | "type": "symbol", 63 | "source": "states", 64 | "minzoom": 4, 65 | "maxzoom": 5, 66 | "layout": { 67 | "text-field": "{STATE_ABBR}", 68 | "text-size": 12, 69 | "text-font": ["Arial Normal", "sans-serif Normal"] 70 | } 71 | }, 72 | { 73 | "id": "state_names", 74 | "type": "symbol", 75 | "source": "states", 76 | "minzoom": 5, 77 | "layout": { 78 | "text-field": ["concat", ["get", "STATE_ABBR"], "\n", ["get", "STATE_NAME"]], 79 | "text-size": 12, 80 | "text-font": ["Arial Normal", "sans-serif Normal"] 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /test/fixtures/hospital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/hospital.png -------------------------------------------------------------------------------- /test/fixtures/hot-osm/osm.json: -------------------------------------------------------------------------------- 1 | {"attribution":"OpenStreetMap","bounds":[-180,-85.0511,180,85.0511],"center":[-76.275329586789,39.153492567373,8],"format":"pbf","minzoom":0,"maxzoom":20,"name":"osm","description":null,"scheme":"xyz","tilejson":"2.1.0","tiles":["https://osm-lambda.tegola.io/v1/maps/osm/{z}/{x}/{y}.pbf"],"grids":[],"data":[],"version":"1.0.0","template":null,"legend":null,"vector_layers":[{"version":2,"extent":4096,"id":"populated_places","name":"populated_places","geometry_type":"point","minzoom":0,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/populated_places/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"country_lines","name":"country_lines","geometry_type":"line","minzoom":0,"maxzoom":10,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/country_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"country_lines_disputed","name":"country_lines_disputed","geometry_type":"line","minzoom":3,"maxzoom":10,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/country_lines_disputed/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"country_label_points","name":"country_label_points","geometry_type":"point","minzoom":3,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/country_label_points/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"country_polygons","name":"country_polygons","geometry_type":"polygon","minzoom":0,"maxzoom":10,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/country_polygons/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"state_lines","name":"state_lines","geometry_type":"line","minzoom":0,"maxzoom":10,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/state_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"land","name":"land","geometry_type":"polygon","minzoom":0,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/land/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"admin_lines","name":"admin_lines","geometry_type":"polygon","minzoom":8,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/admin_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"state_label_points","name":"state_label_points","geometry_type":"point","minzoom":3,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/state_label_points/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"landuse_areas","name":"landuse_areas","geometry_type":"polygon","minzoom":3,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/landuse_areas/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"water_areas","name":"water_areas","geometry_type":"polygon","minzoom":3,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/water_areas/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"water_lines","name":"water_lines","geometry_type":"line","minzoom":8,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/water_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"transport_lines","name":"transport_lines","geometry_type":"line","minzoom":3,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/transport_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"transport_areas","name":"transport_areas","geometry_type":"polygon","minzoom":12,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/transport_areas/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"transport_points","name":"transport_points","geometry_type":"point","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/transport_points/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"amenity_areas","name":"amenity_areas","geometry_type":"polygon","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/amenity_areas/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"amenity_points","name":"amenity_points","geometry_type":"point","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/amenity_points/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"other_points","name":"other_points","geometry_type":"point","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/other_points/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"other_lines","name":"other_lines","geometry_type":"line","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/other_lines/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"other_areas","name":"other_areas","geometry_type":"polygon","minzoom":6,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/other_areas/{z}/{x}/{y}.pbf"]},{"version":2,"extent":4096,"id":"buildings","name":"buildings","geometry_type":"polygon","minzoom":14,"maxzoom":20,"tiles":["https://osm-lambda.tegola.io/v1/maps/osm/buildings/{z}/{x}/{y}.pbf"]}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/hot-osm/osm_tegola_spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/hot-osm/osm_tegola_spritesheet.png -------------------------------------------------------------------------------- /test/fixtures/hot-osm/osm_tegola_spritesheet@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/hot-osm/osm_tegola_spritesheet@2x.png -------------------------------------------------------------------------------- /test/fixtures/osm-liberty/osm-liberty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/osm-liberty/osm-liberty.png -------------------------------------------------------------------------------- /test/fixtures/osm-liberty/osm-liberty@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/osm-liberty/osm-liberty@2x.png -------------------------------------------------------------------------------- /test/fixtures/raster-dem.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "raster-dem", 4 | "center": [-98.78906130124426, 37.92686191312036], 5 | "zoom": 4, 6 | "sources": { 7 | "dem": { 8 | "type": "raster-dem", 9 | "tiles": [ 10 | "{z}/{x}/{y}.png" 11 | ] 12 | } 13 | }, 14 | "layers": [ 15 | { 16 | "id": "hillshading", 17 | "type": "hillshade", 18 | "source": "dem", 19 | "paint": { 20 | "hillshade-exaggeration": 0.5, 21 | "hillshade-light-direction": 45 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/response.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/sprites.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 20, "pixelRatio": 1, "x": 0, "height": 20}, "amenity_firestation": {"y": 0, "width": 50, "pixelRatio": 1, "x": 20, "height": 50}} -------------------------------------------------------------------------------- /test/fixtures/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/sprites.png -------------------------------------------------------------------------------- /test/fixtures/sprites@2x.json: -------------------------------------------------------------------------------- 1 | {"accommodation_camping": {"y": 0, "width": 40, "pixelRatio": 2, "x": 0, "height": 40}, "amenity_firestation": {"y": 0, "width": 100, "pixelRatio": 2, "x": 40, "height": 100}} 2 | -------------------------------------------------------------------------------- /test/fixtures/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlayers/ol-mapbox-style/6181eab3679569a3baa36912f0ab90ca40b6f8ca/test/fixtures/sprites@2x.png -------------------------------------------------------------------------------- /test/fixtures/states.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states", 4 | "sources": { 5 | "states": { 6 | "type": "geojson", 7 | "data": "./states.geojson" 8 | } 9 | }, 10 | "layers": [ 11 | { 12 | "id": "background", 13 | "type": "background", 14 | "paint": { 15 | "background-color": "rgba(0,0,0,0)" 16 | } 17 | }, 18 | { 19 | "id": "population_lt_2m", 20 | "type": "fill", 21 | "source": "states", 22 | "filter": ["<=", "PERSONS", 2000000], 23 | "paint": { 24 | "fill-color": "#A6CEE3", 25 | "fill-opacity": 0.7 26 | } 27 | }, 28 | { 29 | "id": "2m_lt_population_lte_4m", 30 | "type": "fill", 31 | "source": "states", 32 | "filter": ["all", [">", "PERSONS", 2000000], ["<=", "PERSONS", 4000000]], 33 | "paint": { 34 | "fill-color": "#0F78B4", 35 | "fill-opacity": 0.7 36 | } 37 | }, 38 | { 39 | "id": "population_gt_4m", 40 | "type": "fill", 41 | "source": "states", 42 | "filter": [">", "PERSONS", 4000000], 43 | "paint": { 44 | "fill-color": "#B2DF8A", 45 | "fill-opacity": 0.7 46 | } 47 | }, 48 | { 49 | "id": "state_outlines", 50 | "type": "line", 51 | "source": "states", 52 | "paint": { 53 | "line-color": "#8cadbf", 54 | "line-width": 0.1 55 | } 56 | }, 57 | { 58 | "id": "state_abbreviations", 59 | "type": "fill", 60 | "source": "states", 61 | "minzoom": 4, 62 | "maxzoom": 5, 63 | "layout": { 64 | "text-field": "{STATE_ABBR}", 65 | "text-size": 12, 66 | "text-font": ["Arial Normal", "sans-serif Normal"] 67 | } 68 | }, 69 | { 70 | "id": "state_names", 71 | "type": "fill", 72 | "source": "states", 73 | "minzoom": 5, 74 | "layout": { 75 | "visibility": "none", 76 | "text-field": "{STATE_NAME}", 77 | "text-size": 12, 78 | "text-font": ["Arial Normal", "sans-serif Normal"] 79 | } 80 | }, 81 | { 82 | "id": "has_male", 83 | "type": "fill", 84 | "source": "states", 85 | "filter": ["has", "MALE"], 86 | "paint": { 87 | "fill-color": "#000000", 88 | "fill-opacity": 0.001 89 | } 90 | }, 91 | { 92 | "id": "not_has_male", 93 | "type": "fill", 94 | "source": "states", 95 | "filter": ["!has", "MALE"], 96 | "paint": { 97 | "fill-color": "#000000", 98 | "fill-opacity": 0.001 99 | } 100 | } 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /test/fixtures/style-with-multiple-background-layers.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Toner", 4 | "metadata": { 5 | "mapbox:autocomposite": false, 6 | "mapbox:type": "template", 7 | "openmaptiles:version": "3.x", 8 | "openmaptiles:mapbox:owner": "openmaptiles", 9 | "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t" 10 | }, 11 | "center": [ 12 | 20.838971352362933, 13 | 47.241654485577584 14 | ], 15 | "zoom": 2.8784041883593225, 16 | "bearing": 0, 17 | "pitch": 0, 18 | "sources": { 19 | "openmaptiles": { 20 | "type": "vector", 21 | "url": "https://free.tilehosting.com/data/v3.json?key={key}" 22 | } 23 | }, 24 | "glyphs": "https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key={key}", 25 | "layers": [ 26 | { 27 | "id": "background-1", 28 | "type": "background", 29 | "paint": { 30 | "background-color": "blue" 31 | } 32 | }, 33 | { 34 | "id": "background-2", 35 | "type": "background", 36 | "paint": { 37 | "background-color": "yellow" 38 | } 39 | }, 40 | ], 41 | "id": "ciwigmbgt00fw2ps58s6klalp" 42 | } 43 | -------------------------------------------------------------------------------- /test/fixtures/tilejson-mapboxvector.json: -------------------------------------------------------------------------------- 1 | {"attribution":"","bounds":[-180,-85.05112877980659,180,85.05112877980659],"center":[0,0,4],"description":"One of the example maps that comes with TileMill - a bright & colorful world map that blends retro and high-tech with its folded paper texture and interactive flag tooltips. ","download":"http://a.tiles.mapbox.com/v3/mapbox.geography-class.mbtiles","embed":"http://a.tiles.mapbox.com/v3/mapbox.geography-class.html","filesize":134727680,"grids":["http://a.tiles.mapbox.com/v3/mapbox.geography-class/{z}/{x}/{y}.grid.json","http://b.tiles.mapbox.com/v3/mapbox.geography-class/{z}/{x}/{y}.grid.json"],"id":"mapbox.geography-class","legend":"
\n\n
Geography Class
\n
by MapBox
\n\n\n
","maxzoom":8,"minzoom":0,"name":"Geography Class","private":false,"scheme":"xyz","template":"{{#__location__}}{{/__location__}}{{#__teaser__}}
\n\n
\n{{admin}}\n\n
{{/__teaser__}}{{#__full__}}{{/__full__}}","tilejson":"2.0.0","tiles":["http://a.tiles.mapbox.com/v3/mapbox.geography-class/{z}/{x}/{y}.png","http://b.tiles.mapbox.com/v3/mapbox.geography-class/{z}/{x}/{y}.png"],"version":"1.0.0","webpage":"http://a.tiles.mapbox.com/v3/mapbox.geography-class/page.html"} -------------------------------------------------------------------------------- /test/fixtures/tilejson.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "tilejson", 4 | "center": [0, 0], 5 | "zoom": 2, 6 | "sources": { 7 | "tilejson": { 8 | "type": "raster", 9 | "url": "./geography-class.json" 10 | } 11 | }, 12 | "layers": [ 13 | { 14 | "id": "background", 15 | "type": "background", 16 | "paint": { 17 | "background-color": "rgba(0,0,0,0)" 18 | } 19 | }, 20 | { 21 | "id": "tilejson-layer", 22 | "type": "raster", 23 | "source": "tilejson" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/tilejson.raster.json: -------------------------------------------------------------------------------- 1 | { 2 | "tilejson": "2.2.0", 3 | "tiles": [ 4 | "path/to/tiles/{z}/{x}/{y}.png" 5 | ], 6 | "maxzoom": 20, 7 | "minzoom": 0 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/wms.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "states-wms", 4 | "center": [-98.78906130124426, 37.92686191312036], 5 | "zoom": 4, 6 | "sources": { 7 | "osm": { 8 | "type": "raster", 9 | "attribution": "© OpenStreetMap contributors.", 10 | "tileSize": 256, 11 | "tiles": [ 12 | "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png", 13 | "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png", 14 | "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png" 15 | ] 16 | }, 17 | "states": { 18 | "type": "raster", 19 | "maxzoom": 12, 20 | "tileSize": 256, 21 | "tiles": ["https://ahocevar.com/geoserver/gwc/service/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image/png&SRS=EPSG:900913&LAYERS=topp:states&STYLES=&WIDTH=256&HEIGHT=256&BBOX={bbox-epsg-3857}"] 22 | } 23 | }, 24 | "layers": [ 25 | { 26 | "id": "background", 27 | "type": "background", 28 | "paint": { 29 | "background-color": "rgba(0,0,0,0)" 30 | } 31 | }, 32 | { 33 | "id": "osm", 34 | "type": "raster", 35 | "source": "osm" 36 | }, 37 | { 38 | "id": "states-wms", 39 | "type": "raster", 40 | "source": "states", 41 | "paint": { 42 | "raster-opacity": 1 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/index_test.cjs: -------------------------------------------------------------------------------- 1 | // require all modules ending in ".test.js" from the 2 | // current directory and all subdirectories 3 | const testsContext = require.context('./', true, /\.test\.js$/); 4 | 5 | testsContext.keys().forEach(testsContext); 6 | -------------------------------------------------------------------------------- /test/karma.conf.cjs: -------------------------------------------------------------------------------- 1 | const {join} = require('path'); 2 | const puppeteer = require('puppeteer'); 3 | 4 | process.env.CHROME_BIN = puppeteer.executablePath(); 5 | 6 | const flags = ['--headless=new']; 7 | if (process.env.CI) { 8 | flags.push('--no-sandbox'); 9 | } 10 | 11 | module.exports = function (karma) { 12 | karma.set({ 13 | browsers: ['ChromeHeadless'], 14 | customLaunchers: { 15 | ChromeHeadless: { 16 | base: 'Chrome', 17 | flags, 18 | }, 19 | }, 20 | browserDisconnectTolerance: 2, 21 | frameworks: ['webpack', 'mocha'], 22 | client: { 23 | runInParent: true, 24 | mocha: { 25 | timeout: 2500, 26 | }, 27 | }, 28 | files: [ 29 | { 30 | pattern: '**/*.test.js', 31 | watched: false, 32 | }, 33 | { 34 | pattern: '**/*', 35 | included: false, 36 | watched: false, 37 | }, 38 | ], 39 | proxies: { 40 | '/fixtures/': '/base/fixtures/', 41 | }, 42 | preprocessors: { 43 | '**/*.js': ['webpack', 'sourcemap'], 44 | }, 45 | plugins: [ 46 | 'karma-chrome-launcher', 47 | 'karma-mocha', 48 | 'karma-webpack', 49 | 'karma-sourcemap-loader', 50 | 'karma-coverage-istanbul-reporter', 51 | ], 52 | reporters: ['dots', 'coverage-istanbul'], 53 | coverageIstanbulReporter: { 54 | reports: ['html', 'text-summary'], 55 | dir: join(__dirname, '..', 'coverage'), 56 | fixWebpackSourcePaths: true, 57 | }, 58 | webpack: { 59 | devtool: 'inline-source-map', 60 | mode: 'development', 61 | module: { 62 | rules: [ 63 | { 64 | test: /\.js/, 65 | include: /src/, 66 | exclude: /node_modules|\.test\.js$/, 67 | use: '@jsdevtools/coverage-istanbul-loader', 68 | }, 69 | { 70 | test: /\.js$/, 71 | enforce: 'pre', 72 | use: ['remove-flow-types-loader'], 73 | include: join( 74 | __dirname, 75 | '..', 76 | 'node_modules', 77 | '@mapbox', 78 | 'mapbox-gl-style-spec', 79 | ), 80 | }, 81 | ], 82 | }, 83 | resolve: { 84 | fallback: { 85 | 'assert': join(__dirname, '..', 'node_modules', 'nanoassert'), 86 | }, 87 | }, 88 | }, 89 | webpackMiddleware: { 90 | noInfo: true, 91 | }, 92 | }); 93 | 94 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 95 | }; 96 | -------------------------------------------------------------------------------- /test/mapbox.test.js: -------------------------------------------------------------------------------- 1 | import should from 'should'; 2 | import { 3 | getMapboxPath, 4 | normalizeSourceUrl, 5 | normalizeSpriteDefinition, 6 | normalizeSpriteUrl, 7 | normalizeStyleUrl, 8 | } from '../src/mapbox.js'; 9 | 10 | describe('Mapbox utilities', function () { 11 | describe('getMapboxPath()', () => { 12 | const cases = [ 13 | { 14 | url: 'mapbox://path/to/resource', 15 | expected: 'path/to/resource', 16 | }, 17 | { 18 | url: 'mapbox://path/to/resource?query', 19 | expected: 'path/to/resource?query', 20 | }, 21 | { 22 | url: 'https://example.com/resource', 23 | expected: '', 24 | }, 25 | ]; 26 | 27 | for (const c of cases) { 28 | it(`works for ${c.url}`, () => { 29 | should(getMapboxPath(c.url)).equal(c.expected); 30 | }); 31 | } 32 | }); 33 | 34 | describe('normalizeStyleUrl()', () => { 35 | const cases = [ 36 | { 37 | url: 'mapbox://styles/mapbox/bright-v9', 38 | expected: 39 | 'https://api.mapbox.com/styles/v1/mapbox/bright-v9?&access_token=test-token', 40 | }, 41 | { 42 | url: 'https://example.com/style', 43 | expected: 'https://example.com/style', 44 | }, 45 | ]; 46 | 47 | const token = 'test-token'; 48 | for (const c of cases) { 49 | it(`works for ${c.url}`, () => { 50 | should(normalizeStyleUrl(c.url, token)).equal(c.expected); 51 | }); 52 | } 53 | }); 54 | 55 | describe('normalizeSpriteUrl()', () => { 56 | const cases = [ 57 | { 58 | url: 'mapbox://sprites/mapbox/bright-v9', 59 | expected: 60 | 'https://api.mapbox.com/styles/v1/mapbox/bright-v9/sprite?access_token=test-token', 61 | }, 62 | { 63 | url: 'https://example.com/sprite', 64 | expected: 'https://example.com/sprite', 65 | }, 66 | { 67 | url: '../sprite', 68 | expected: 'https://example.com:8000/sprite', 69 | }, 70 | { 71 | url: '/sprite', 72 | expected: 'https://example.com:8000/sprite', 73 | }, 74 | { 75 | url: './sprite', 76 | expected: 'https://example.com:8000/mystyle/sprite', 77 | }, 78 | ]; 79 | 80 | const token = 'test-token'; 81 | for (const c of cases) { 82 | it(`works for ${c.url}`, () => { 83 | should( 84 | normalizeSpriteUrl( 85 | c.url, 86 | token, 87 | 'https://example.com:8000/mystyle/style.json', 88 | ), 89 | ).equal(c.expected); 90 | }); 91 | } 92 | }); 93 | 94 | describe('normalizeSpriteDefinition()', () => { 95 | const cases = [ 96 | { 97 | sprite: 'mapbox://sprites/mapbox/bright-v9', 98 | expected: [ 99 | { 100 | id: 'default', 101 | url: 'https://api.mapbox.com/styles/v1/mapbox/bright-v9/sprite?access_token=test-token', 102 | }, 103 | ], 104 | }, 105 | { 106 | sprite: [ 107 | { 108 | id: 'base', 109 | url: 'mapbox://sprites/mapbox/bright-v9', 110 | }, 111 | ], 112 | expected: [ 113 | { 114 | id: 'base', 115 | url: 'https://api.mapbox.com/styles/v1/mapbox/bright-v9/sprite?access_token=test-token', 116 | }, 117 | ], 118 | }, 119 | { 120 | sprite: [ 121 | { 122 | id: 'example', 123 | url: 'https://example.com/sprite', 124 | }, 125 | { 126 | id: 'local', 127 | url: '../sprite', 128 | }, 129 | ], 130 | expected: [ 131 | { 132 | id: 'example', 133 | url: 'https://example.com/sprite', 134 | }, 135 | { 136 | id: 'local', 137 | url: 'https://example.com:8000/sprite', 138 | }, 139 | ], 140 | }, 141 | ]; 142 | 143 | const token = 'test-token'; 144 | for (const c of cases) { 145 | it(`works for ${c.sprite}`, () => { 146 | should( 147 | normalizeSpriteDefinition( 148 | c.sprite, 149 | token, 150 | 'https://example.com:8000/mystyle/style.json', 151 | ), 152 | ).deepEqual(c.expected); 153 | }); 154 | } 155 | }); 156 | 157 | describe('normalizeSourceUrl()', () => { 158 | const cases = [ 159 | { 160 | url: 'mapbox://mapbox.mapbox-streets-v7', 161 | expected: [ 162 | 'https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=test-token', 163 | 'https://b.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=test-token', 164 | 'https://c.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=test-token', 165 | 'https://d.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=test-token', 166 | ], 167 | }, 168 | { 169 | url: 'https://example.com/source/{z}/{x}/{y}.pbf', 170 | expected: [ 171 | 'https://example.com/source/{z}/{x}/{y}.pbf?token=test-token', 172 | ], 173 | }, 174 | { 175 | url: 'https://example.com/source/{z}/{x}/{y}.pbf?foo=bar', 176 | expected: [ 177 | 'https://example.com/source/{z}/{x}/{y}.pbf?foo=bar&token=test-token', 178 | ], 179 | }, 180 | { 181 | url: [ 182 | 'https://example.com/source/{z}/{x}/{y}.pbf?token=override-token', 183 | ], 184 | expected: [ 185 | 'https://example.com/source/{z}/{x}/{y}.pbf?token=override-token', 186 | ], 187 | }, 188 | ]; 189 | 190 | const token = 'test-token'; 191 | const tokenParam = 'token'; 192 | for (const c of cases) { 193 | it(`works for ${c.url}`, () => { 194 | should( 195 | normalizeSourceUrl(c.url, token, tokenParam, location.href), 196 | ).deepEqual(c.expected); 197 | }); 198 | } 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/stylefunction-utils.test.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: "off"*/ 2 | import {Color, v8 as spec} from '@maplibre/maplibre-gl-style-spec'; 3 | import Feature from 'ol/Feature.js'; 4 | import Point from 'ol/geom/Point.js'; 5 | import should from 'should'; 6 | 7 | import { 8 | _colorWithOpacity as colorWithOpacity, 9 | _evaluateFilter as evaluateFilter, 10 | _fromTemplate as fromTemplate, 11 | _getValue as getValue, 12 | cameraObj, 13 | } from '../src/stylefunction.js'; 14 | 15 | describe('utility functions currently in stylefunction.js', function () { 16 | describe('colorWithOpacity()', function () { 17 | it('should parse Color instances', function () { 18 | should(colorWithOpacity(new Color(1, 0, 0, 1), 1)).eql('rgba(255,0,0,1)'); 19 | should(colorWithOpacity(new Color(1, 0, 0, 1), 0.25)).eql( 20 | 'rgba(255,0,0,0.25)', 21 | ); 22 | should(colorWithOpacity(new Color(1, 0, 0, 1))).eql('rgba(255,0,0,1)'); 23 | should(colorWithOpacity(new Color(1, 0, 0, 1))).eql('rgba(255,0,0,1)'); 24 | }); 25 | 26 | it('should return undefined if alpha or opacity is 0', function () { 27 | should(colorWithOpacity(new Color(1, 0, 0, 0), 1)).eql(undefined); 28 | should(colorWithOpacity(new Color(1, 0, 0, 1), 0)).eql(undefined); 29 | }); 30 | }); 31 | 32 | describe('evaluateFilter()', function () { 33 | const filterCache = {}; 34 | const feature = new Feature({geometry: new Point([0, 0], 'XY')}); 35 | cameraObj.zoom = 11; 36 | 37 | it('should be true with "all" filter', function () { 38 | const glLayerId = 'gl-layer-id'; 39 | const filter = '[ "all" ]'; 40 | 41 | should(filterCache).not.have.key(glLayerId); 42 | should(evaluateFilter(glLayerId, filter, feature, filterCache)).be.true; 43 | should(filterCache).have.key(glLayerId); 44 | }); 45 | 46 | it('should be false with LineString filter and Point geom', function () { 47 | const glLayerId = 'gl-layer-id-2'; 48 | const filter = '[ "==", "$type", "LineString" ]'; 49 | 50 | should(evaluateFilter(glLayerId, filter, feature, filterCache)).be.false; 51 | should(filterCache).have.key(glLayerId); 52 | }); 53 | 54 | it('should be true with Point filter and Point geom', function () { 55 | const glLayerId = 'gl-layer-id-2'; 56 | const filter = '[ "==", "$type", "Point" ]'; 57 | 58 | should(evaluateFilter(glLayerId, filter, feature, filterCache)).be.false; 59 | should(filterCache).have.key(glLayerId); 60 | }); 61 | }); 62 | 63 | describe('fromTemplate()', function () { 64 | const props = {de: 'BLAH', fun: 'not fun'}; 65 | 66 | it('should replace single template string', function () { 67 | const tmpl = 'blah, blah, blah {de} blah'; 68 | should.equal(tmpl.replace('{de}', 'BLAH'), fromTemplate(tmpl, props)); 69 | }); 70 | 71 | it('should replace two subs in template string', function () { 72 | const tmpl = 'blah, blah, blah {de} blah fun fun {fun}'; 73 | should.equal( 74 | tmpl.replace('{de}', 'BLAH').replace('{fun}', 'not fun'), 75 | fromTemplate(tmpl, props), 76 | ); 77 | }); 78 | 79 | it('should handle templates with no subs', function () { 80 | const tmpl = 'blah, blah, blah de blah fun fun fun'; 81 | should.equal(tmpl, fromTemplate(tmpl, props)); 82 | }); 83 | 84 | it('should remove subs with no matching properties', function () { 85 | const tmpl = 'blah, blah, {what}blah de blah fun fun fun'; 86 | const result = 'blah, blah, blah de blah fun fun fun'; 87 | should.equal(result, fromTemplate(tmpl, props)); 88 | }); 89 | 90 | it('should handle minorly misshapen subs', function () { 91 | const tmpl = 'blah, blah, blah {de blah fun {fun} fun'; 92 | should.equal(tmpl.replace('{fun}', 'not fun'), fromTemplate(tmpl, props)); 93 | }); 94 | }); 95 | 96 | describe('getValue()', function () { 97 | const feature = new Feature({geometry: new Point([0, 0], 'XY')}); 98 | const functionCache = {}; 99 | const glLayer = { 100 | 'id': 'landuse-residential', 101 | 'layout': { 102 | 'visibility': 'visible', 103 | }, 104 | 'paint': { 105 | 'fill-color': 'rgba(192, 216, 151, 0.53)', 106 | 'fill-opacity': 0.7, 107 | }, 108 | 'type': 'fill', 109 | }; 110 | const glLayer2 = { 111 | 'id': 'park_outline', 112 | 'paint': { 113 | 'line-color': 'rgba(159, 183, 118, 0.69)', 114 | 'line-gap-width': { 115 | 'stops': [ 116 | [12, 0], 117 | [20, 6], 118 | ], 119 | }, 120 | }, 121 | 'type': 'line', 122 | }; 123 | 124 | beforeEach(function () { 125 | cameraObj.zoom = 11; 126 | }); 127 | 128 | it('should get correct default property', function () { 129 | const d = spec['layout_line']['line-cap']['default']; 130 | 131 | should.equal( 132 | getValue(glLayer2, 'layout', 'line-cap', feature, functionCache), 133 | d, 134 | ); 135 | should(functionCache).have.key(glLayer2.id); 136 | }); 137 | 138 | it('should get simple layout property', function () { 139 | should.equal( 140 | getValue(glLayer, 'layout', 'visibility', feature, functionCache), 141 | 'visible', 142 | ); 143 | should(functionCache).have.key(glLayer.id); 144 | }); 145 | 146 | it('should get simple paint property', function () { 147 | should.equal(getValue(glLayer, 'paint', 'fill-opacity', feature), 0.7); 148 | }); 149 | 150 | it('should get color paint property', function () { 151 | const result = getValue(glLayer, 'paint', 'fill-color', feature); 152 | should(result).be.instanceof(Color); 153 | }); 154 | 155 | it('should get complex paint property', function () { 156 | cameraObj.zoom = 20; 157 | const result = getValue(glLayer2, 'paint', 'line-gap-width', feature); 158 | should(result).equal(6); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test of the standalone build 5 | 6 | 7 | 8 |
9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/text.test.js: -------------------------------------------------------------------------------- 1 | import should from 'should'; 2 | import {getFonts, wrapText} from '../src/text.js'; 3 | import {createObserver} from './util.test.js'; 4 | 5 | describe('text', function () { 6 | describe('wrapText()', function () { 7 | it('properly wraps text', function () { 8 | const text = 'Verylongtext i i longtext short Shor T i i'; 9 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 10 | should(result).equal('Verylongtext i i\nlongtext short\nShor T i i'); 11 | }); 12 | 13 | it('should not produce undefined lines', function () { 14 | const text = 'Shor T'; 15 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 16 | should(result).equal(text); 17 | }); 18 | 19 | it('should not combine re-combined lines when last is empty', function () { 20 | const text = 'i i'; 21 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 22 | should(result).equal(text); 23 | }); 24 | 25 | it('should wrap text', function () { 26 | const text = 'Longer line of text for wrapping'; 27 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 28 | result.includes('\n').should.be.true(); 29 | }); 30 | 31 | it('should preserve hard breaks', function () { 32 | const text = 'Großer Sonnleitstein\n1639 m'; 33 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 34 | should(result).equal('Großer\nSonnleitstein\n1639 m'); 35 | }); 36 | 37 | it('should combine lines with less than 30% max width', function () { 38 | const text = 'Single_Long_Word 30%'; 39 | const result = wrapText(text, 'normal 400 12px/1.2 sans-serif', 10, 0); 40 | should(result).equal(text); 41 | }); 42 | }); 43 | 44 | describe('getFonts', function () { 45 | beforeEach(function () { 46 | const stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 47 | stylesheets.forEach(function (stylesheet) { 48 | stylesheet.remove(); 49 | }); 50 | document.fonts.clear(); 51 | }); 52 | 53 | it('does not load standard fonts', function () { 54 | getFonts(['monospace', 'sans-serif']); 55 | const stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 56 | should(stylesheets.length).eql(0); 57 | }); 58 | 59 | it('loads fonts with a template using {Font+Family} and {fontstyle}', async function () { 60 | getFonts( 61 | [ 62 | 'Noto Sans Bold', 63 | 'Noto Sans Regular Italic', 64 | 'Averia Sans Libre Bold', 65 | ], 66 | 'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}', 67 | ); 68 | await createObserver(3); 69 | let stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 70 | should(stylesheets.length).eql(3); 71 | should(stylesheets.item(0).href).eql( 72 | 'https://fonts.googleapis.com/css?family=Noto+Sans:700normal', 73 | ); 74 | should(stylesheets.item(1).href).eql( 75 | 'https://fonts.googleapis.com/css?family=Noto+Sans:400italic', 76 | ); 77 | should(stylesheets.item(2).href).eql( 78 | 'https://fonts.googleapis.com/css?family=Averia+Sans+Libre:700normal', 79 | ); 80 | 81 | // Does not load the same font twice 82 | getFonts( 83 | ['Noto Sans Bold'], 84 | 'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}', 85 | ); 86 | await new Promise((resolve) => setTimeout(resolve, 500)); 87 | stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 88 | should(stylesheets.length).eql(3); 89 | }); 90 | 91 | it('loads fonts with a template using {font-family} and {-fontstyle}', async function () { 92 | getFonts( 93 | ['Noto Sans Regular', 'Averia Sans Libre Bold Italic'], 94 | './fonts/{font-family}/{fontweight}{-fontstyle}.css', 95 | ); 96 | await createObserver(2); 97 | const stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 98 | should(stylesheets.length).eql(2); 99 | should(stylesheets.item(0).href).eql( 100 | location.origin + '/fonts/noto-sans/400.css', 101 | ); 102 | should(stylesheets.item(1).href).eql( 103 | location.origin + '/fonts/averia-sans-libre/700-italic.css', 104 | ); 105 | }); 106 | 107 | it('uses the default template if none is provided', async function () { 108 | getFonts(['Averia Sans Libre']); 109 | await createObserver(1); 110 | const stylesheets = document.querySelectorAll('link[rel=stylesheet]'); 111 | should(stylesheets.length).eql(1); 112 | should(stylesheets.item(0).href).eql( 113 | 'https://cdn.jsdelivr.net/npm/@fontsource/averia-sans-libre/400.css', 114 | ); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | import brightV9 from 'mapbox-gl-styles/styles/bright-v9.json'; 2 | import {Feature} from 'ol'; 3 | import Map from 'ol/Map.js'; 4 | import {Polygon} from 'ol/geom.js'; 5 | import VectorLayer from 'ol/layer/Vector.js'; 6 | import VectorTileLayer from 'ol/layer/VectorTile.js'; 7 | import VectorTileSource from 'ol/source/VectorTile.js'; 8 | import should from 'should'; 9 | import { 10 | addMapboxLayer, 11 | apply, 12 | getLayer, 13 | getLayers, 14 | getMapboxLayer, 15 | getSource, 16 | removeMapboxLayer, 17 | setupVectorSource, 18 | updateMapboxLayer, 19 | updateMapboxSource, 20 | } from '../src/apply.js'; 21 | import {fetchResource} from '../src/util.js'; 22 | 23 | export function createObserver(total) { 24 | return new Promise((resolve) => { 25 | let count = 0; 26 | const observer = new MutationObserver(() => { 27 | count++; 28 | if (count === total) { 29 | observer.disconnect(); 30 | resolve(); 31 | } 32 | }); 33 | observer.observe(document.head, { 34 | childList: true, 35 | subtree: true, 36 | }); 37 | }); 38 | } 39 | 40 | describe('util', function () { 41 | describe('fetchResource', function () { 42 | it('allows to transform requests with the transformRequest option', function (done) { 43 | fetchResource('Sprite', 'my://resource', { 44 | transformRequest: function (url, resourceType) { 45 | should(url).equal('my://resource'); 46 | should(resourceType).equal('Sprite'); 47 | return new Request('/fixtures/sprites.json'); 48 | }, 49 | }).then(function (sprite) { 50 | should(typeof sprite.accommodation_camping).equal('object'); 51 | done(); 52 | }); 53 | }); 54 | it('adds the request to the metadata for both pending and new requests', function (done) { 55 | const metadataNotPending = {}; 56 | const metadataPending = {}; 57 | Promise.all([ 58 | fetchResource( 59 | 'Sprite', 60 | 'my://resource', 61 | { 62 | transformRequest: function (url, resourceType) { 63 | should(url).equal('my://resource'); 64 | should(resourceType).equal('Sprite'); 65 | return new Request('/fixtures/sprites.json'); 66 | }, 67 | }, 68 | metadataNotPending, 69 | ), 70 | fetchResource( 71 | 'Sprite', 72 | 'my://resource', 73 | { 74 | transformRequest: function (url, resourceType) { 75 | should(url).equal('my://resource'); 76 | should(resourceType).equal('Sprite'); 77 | return new Request('/fixtures/sprites.json'); 78 | }, 79 | }, 80 | metadataPending, 81 | ), 82 | ]) 83 | .then(() => { 84 | should('url' in metadataPending).true(); 85 | should(metadataPending.request).equal(metadataNotPending.request); 86 | done(); 87 | }) 88 | .catch((err) => done(err)); 89 | }); 90 | }); 91 | describe('getTileJson', function () { 92 | it('resolves mapbox:// tile urls properly', function (done) { 93 | setupVectorSource( 94 | { 95 | url: 'mapbox://mapbox.mapbox-streets-v7', 96 | type: 'vector', 97 | }, 98 | location.href + '?getTileJson', 99 | { 100 | accessToken: 'mytoken', 101 | }, 102 | ) 103 | .then(function (source) { 104 | const url = source.getTileUrlFunction()([0, 0, 0]); 105 | should(url).eql( 106 | 'https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/0/0/0.vector.pbf?access_token=mytoken', 107 | ); 108 | done(); 109 | }) 110 | .catch((err) => done(err)); 111 | }); 112 | }); 113 | 114 | describe('getLayer', function () { 115 | let target; 116 | beforeEach(function () { 117 | target = document.createElement('div'); 118 | }); 119 | 120 | it('returns a layer', function (done) { 121 | apply(target, brightV9) 122 | .then(function (map) { 123 | // add another layer that has no 'mapbox-layers' set 124 | map.addLayer(new VectorTileLayer()); 125 | should( 126 | getLayer(map, 'landuse_park') 127 | .get('mapbox-layers') 128 | .indexOf('landuse_park'), 129 | ).equal(1); 130 | done(); 131 | }) 132 | .catch(function (error) { 133 | done(error); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('getLayers', function () { 139 | let target; 140 | beforeEach(function () { 141 | target = document.createElement('div'); 142 | }); 143 | 144 | it('returns an array of layers', function (done) { 145 | apply(target, brightV9) 146 | .then(function (map) { 147 | // add another layer that has no 'mapbox-layers' set 148 | map.addLayer(new VectorTileLayer()); 149 | const layers = getLayers(map, 'mapbox'); 150 | should(layers).be.an.instanceOf(Array); 151 | should(layers[0]).be.an.instanceOf(VectorTileLayer); 152 | should(getLayers(map, 'mapbo').length).eql(0); 153 | done(); 154 | }) 155 | .catch(function (error) { 156 | done(error); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('getSource', function () { 162 | let target; 163 | beforeEach(function () { 164 | target = document.createElement('div'); 165 | }); 166 | 167 | it('returns a source', function (done) { 168 | apply(target, brightV9) 169 | .then(function (map) { 170 | // add another layer that has no 'mapbox-source' set 171 | map.addLayer(new VectorTileLayer()); 172 | should(getSource(map, 'mapbox')).be.an.instanceOf(VectorTileSource); 173 | should(getSource(map, 'mapbo')).be.undefined(); 174 | done(); 175 | }) 176 | .catch(function (error) { 177 | done(error); 178 | }); 179 | }); 180 | }); 181 | 182 | describe('getMapboxLayer', function () { 183 | let target; 184 | beforeEach(function () { 185 | target = document.createElement('div'); 186 | }); 187 | 188 | it('returns a mapbox layer', function (done) { 189 | apply(target, brightV9) 190 | .then(function (map) { 191 | should(getMapboxLayer(map, 'landuse_park').id).eql('landuse_park'); 192 | done(); 193 | }) 194 | .catch(function (error) { 195 | done(error); 196 | }); 197 | }); 198 | }); 199 | 200 | describe('addMapboxLayer', function (done) { 201 | let map; 202 | beforeEach(function (done) { 203 | const target = document.createElement('div'); 204 | map = new Map({ 205 | target: target, 206 | }); 207 | apply(map, JSON.parse(JSON.stringify(brightV9))).then(() => done()); 208 | }); 209 | afterEach(function () { 210 | map.setTarget(null); 211 | }); 212 | 213 | it('adds a mapbox layer before a specific layer', function (done) { 214 | const layer = getLayer(map, 'landuse_park'); 215 | should.equal(layer.get('mapbox-layers').indexOf('landuse_park'), 1); 216 | should.equal( 217 | map 218 | .get('mapbox-style') 219 | .layers.findIndex((l) => l.id === 'landuse_park'), 220 | 2, 221 | ); 222 | const oldRevision = layer.getRevision(); 223 | addMapboxLayer( 224 | map, 225 | { 226 | id: 'inserted', 227 | source: 'mapbox', 228 | }, 229 | 'landuse_park', 230 | ) 231 | .then(() => { 232 | should.deepEqual(getMapboxLayer(map, 'inserted'), { 233 | id: 'inserted', 234 | source: 'mapbox', 235 | }); 236 | should.equal(layer.get('mapbox-layers').indexOf('landuse_park'), 2); 237 | should.equal( 238 | map 239 | .get('mapbox-style') 240 | .layers.findIndex((l) => l.id === 'landuse_park'), 241 | 3, 242 | ); 243 | should.equal(layer.get('mapbox-layers').indexOf('inserted'), 1); 244 | should.equal( 245 | map 246 | .get('mapbox-style') 247 | .layers.findIndex((l) => l.id === 'inserted'), 248 | 2, 249 | ); 250 | should.equal(layer.getRevision(), oldRevision + 1); 251 | done(); 252 | }) 253 | .catch((err) => done(err)); 254 | }); 255 | 256 | it('adds a mapbox layer at the end of the layer stack', function (done) { 257 | const layer = getLayer(map, 'country_label_1'); 258 | addMapboxLayer(map, { 259 | id: 'inserted', 260 | source: 'mapbox', 261 | }) 262 | .then(() => { 263 | should.equal( 264 | map 265 | .get('mapbox-style') 266 | .layers.findIndex((l) => l.id === 'inserted'), 267 | map.get('mapbox-style').layers.length - 1, 268 | ); 269 | should.equal( 270 | layer.get('mapbox-layers').indexOf('inserted'), 271 | layer.get('mapbox-layers').length - 1, 272 | ); 273 | done(); 274 | }) 275 | .catch((err) => done(err)); 276 | }); 277 | }); 278 | 279 | describe('addMapboxLayer with multiple sources', function (done) { 280 | let map; 281 | beforeEach(function (done) { 282 | const target = document.createElement('div'); 283 | map = new Map({ 284 | target: target, 285 | }); 286 | apply(map, { 287 | version: 8, 288 | sources: { 289 | source1: { 290 | type: 'geojson', 291 | data: { 292 | type: 'FeatureCollection', 293 | features: [ 294 | { 295 | type: 'Feature', 296 | properties: {}, 297 | geometry: {type: 'Point', coordinates: [0, 0]}, 298 | }, 299 | ], 300 | }, 301 | }, 302 | source2: { 303 | type: 'geojson', 304 | data: { 305 | type: 'FeatureCollection', 306 | features: [ 307 | { 308 | type: 'Feature', 309 | properties: {}, 310 | geometry: {type: 'Point', coordinates: [16, 48]}, 311 | }, 312 | ], 313 | }, 314 | }, 315 | }, 316 | layers: [ 317 | { 318 | id: 'layer1', 319 | source: 'source1', 320 | type: 'circle', 321 | }, 322 | { 323 | id: 'layer2', 324 | source: 'source2', 325 | type: 'circle', 326 | }, 327 | ], 328 | }) 329 | .then(() => done()) 330 | .catch((err) => done(err)); 331 | }); 332 | afterEach(function () { 333 | map.setTarget(null); 334 | }); 335 | 336 | it('adds a mapbox layer at the beginning of the layer stack', function (done) { 337 | addMapboxLayer( 338 | map, 339 | { 340 | id: 'inserted', 341 | source: 'source1', 342 | }, 343 | 'layer1', 344 | ) 345 | .then(() => { 346 | const layer = getLayer(map, 'inserted'); 347 | should.equal( 348 | map 349 | .get('mapbox-style') 350 | .layers.findIndex((l) => l.id === 'inserted'), 351 | 0, 352 | ); 353 | should.equal(layer.get('mapbox-layers').indexOf('inserted'), 0); 354 | done(); 355 | }) 356 | .catch((err) => done(err)); 357 | }); 358 | 359 | it('adds layers between sources - next layer', function (done) { 360 | addMapboxLayer( 361 | map, 362 | { 363 | id: 'inserted', 364 | source: 'source1', 365 | }, 366 | 'layer2', 367 | ) 368 | .then(() => { 369 | const layer = getLayer(map, 'inserted'); 370 | should.equal(map.getLayers().getArray().indexOf(layer), 0); 371 | should.equal( 372 | map 373 | .get('mapbox-style') 374 | .layers.findIndex((l) => l.id === 'inserted'), 375 | 1, 376 | ); 377 | should.equal(layer.get('mapbox-layers').indexOf('inserted'), 1); 378 | done(); 379 | }) 380 | .catch((err) => done(err)); 381 | }); 382 | 383 | it('adds layers between sources - previous layer', function (done) { 384 | addMapboxLayer( 385 | map, 386 | { 387 | id: 'inserted', 388 | source: 'source2', 389 | }, 390 | 'layer2', 391 | ) 392 | .then(() => { 393 | const layer = getLayer(map, 'inserted'); 394 | should.equal(map.getLayers().getArray().indexOf(layer), 1); 395 | should.equal( 396 | map 397 | .get('mapbox-style') 398 | .layers.findIndex((l) => l.id === 'inserted'), 399 | 1, 400 | ); 401 | should.equal(layer.get('mapbox-layers').indexOf('inserted'), 0); 402 | done(); 403 | }) 404 | .catch((err) => done(err)); 405 | }); 406 | 407 | it('appends an OpenLayers layer for a Mapbox layer', function (done) { 408 | addMapboxLayer(map, { 409 | id: 'inserted', 410 | source: 'source1', 411 | type: 'circle', 412 | }) 413 | .then(() => { 414 | const layer = getLayer(map, 'inserted'); 415 | should(layer).be.instanceOf(VectorLayer); 416 | should.equal(map.getLayers().getArray().length, 3); 417 | should.deepEqual(layer.get('mapbox-layers'), ['inserted']); 418 | should.equal(map.getLayers().getArray().indexOf(layer), 2); 419 | should.equal(layer.getVisible(), true); 420 | done(); 421 | }) 422 | .catch((err) => done(err)); 423 | }); 424 | 425 | it('inserts an OpenLayers layer for a Mapbox layer', function (done) { 426 | map.get('mapbox-style').sources.source3 = { 427 | type: 'geojson', 428 | data: { 429 | type: 'FeatureCollection', 430 | features: [ 431 | { 432 | type: 'Feature', 433 | properties: {}, 434 | geometry: {type: 'Point', coordinates: [3, 60]}, 435 | }, 436 | ], 437 | }, 438 | }; 439 | addMapboxLayer( 440 | map, 441 | { 442 | id: 'inserted', 443 | source: 'source3', 444 | }, 445 | 'layer2', 446 | ) 447 | .then(() => { 448 | const layer = getLayer(map, 'inserted'); 449 | should(layer).be.instanceOf(VectorLayer); 450 | should.equal(map.getLayers().getArray().length, 3); 451 | should.deepEqual(layer.get('mapbox-layers'), ['inserted']); 452 | should.equal(map.getLayers().getArray().indexOf(layer), 1); 453 | should.equal(layer.getVisible(), true); 454 | done(); 455 | }) 456 | .catch((err) => done(err)); 457 | }); 458 | }); 459 | 460 | describe('updateMapboxSource', function () { 461 | let map, target, source1, source2, source3; 462 | beforeEach(function () { 463 | target = document.createElement('div'); 464 | map = new Map({target: target}); 465 | source1 = { 466 | type: 'geojson', 467 | data: { 468 | type: 'FeatureCollection', 469 | features: [ 470 | { 471 | type: 'Feature', 472 | properties: {}, 473 | geometry: {type: 'Point', coordinates: [1, 1]}, 474 | }, 475 | ], 476 | }, 477 | }; 478 | source2 = { 479 | type: 'vector', 480 | tiles: ['http://example.com/{z}/{x}/{y}.pbf'], 481 | }; 482 | source3 = { 483 | type: 'raster', 484 | tiles: ['http://example.com/{z}/{x}/{y}.png'], 485 | }; 486 | return apply(map, { 487 | version: 8, 488 | sources: { 489 | source1: source1, 490 | source2: source2, 491 | source3: source3, 492 | }, 493 | layers: [ 494 | { 495 | id: 'layer1', 496 | source: 'source1', 497 | type: 'circle', 498 | }, 499 | { 500 | id: 'layer2', 501 | source: 'source2', 502 | 'source-layer': 'layer2', 503 | type: 'circle', 504 | }, 505 | { 506 | id: 'layer3', 507 | source: 'source3', 508 | type: 'raster', 509 | }, 510 | ], 511 | }); 512 | }); 513 | it('updates a geojson source', function (done) { 514 | should(getSource(map, 'source1').getFeatures()[0].get('modified')).eql( 515 | undefined, 516 | ); 517 | source1.data.features[0].properties.modified = true; 518 | updateMapboxSource(map, 'source1', source1).then(function () { 519 | try { 520 | const source = getSource(map, 'source1'); 521 | should(source).eql(getLayer(map, 'layer1').getSource()); 522 | should(source.getFeatures()[0].get('modified')).eql(true); 523 | should(); 524 | done(); 525 | } catch (err) { 526 | done(err); 527 | } 528 | }); 529 | }); 530 | it('updates a vector source', function (done) { 531 | should(getSource(map, 'source2').getUrls()[0]).eql( 532 | 'http://example.com/{z}/{x}/{y}.pbf', 533 | ); 534 | source2.tiles[0] = 'http://example.com/{z}/{x}/{y}.mvt'; 535 | updateMapboxSource(map, 'source2', source2).then(function () { 536 | try { 537 | const source = getSource(map, 'source2'); 538 | should(source).eql(getLayer(map, 'layer2').getSource()); 539 | should(source.getUrls()[0]).eql('http://example.com/{z}/{x}/{y}.mvt'); 540 | done(); 541 | } catch (err) { 542 | done(err); 543 | } 544 | }); 545 | }); 546 | it('updates a raster source', function (done) { 547 | should(getSource(map, 'source3').getTileUrlFunction()([0, 0, 0])).eql( 548 | 'http://example.com/0/0/0.png', 549 | ); 550 | source3.tiles[0] = 'http://example.com/{z}/{x}/{y}.jpg'; 551 | updateMapboxSource(map, 'source3', source3).then(function () { 552 | try { 553 | const source = getSource(map, 'source3'); 554 | should(source).eql(getLayer(map, 'layer3').getSource()); 555 | should(source.getTileUrlFunction()([0, 0, 0])).eql( 556 | 'http://example.com/0/0/0.jpg', 557 | ); 558 | done(); 559 | } catch (err) { 560 | done(err); 561 | } 562 | }); 563 | }); 564 | }); 565 | 566 | describe('updateMapboxLayer', function () { 567 | let target; 568 | beforeEach(function () { 569 | target = document.createElement('div'); 570 | }); 571 | 572 | it('updates a geojson source', function (done) { 573 | apply(target, JSON.parse(JSON.stringify(brightV9))) 574 | .then(function (map) { 575 | // add another layer that has no 'mapbox-layers' set 576 | map.addLayer(new VectorTileLayer()); 577 | const layer = getMapboxLayer(map, 'landuse_park'); 578 | layer.paint['fill-color'] = 'red'; 579 | updateMapboxLayer(map, layer); 580 | const getStyle = getLayer(map, 'landuse_park').getStyle(); 581 | const feature = new Feature({ 582 | geometry: new Polygon([ 583 | [ 584 | [0, 0], 585 | [0, 1], 586 | [1, 1], 587 | [1, 0], 588 | [0, 0], 589 | ], 590 | ]), 591 | 'mvt:layer': 'landuse', 592 | class: 'park', 593 | }); 594 | let styles = getStyle(feature, 1); 595 | should(styles[0].getFill().getColor()).eql('rgba(255,0,0,1)'); 596 | layer.paint['fill-color'] = 'blue'; 597 | updateMapboxLayer(map, layer); 598 | styles = getStyle(feature, 1); 599 | should(styles[0].getFill().getColor()).eql('rgba(0,0,255,1)'); 600 | done(); 601 | }) 602 | .catch((err) => done(err)); 603 | }); 604 | 605 | it('updates a mapbox layer with a new object', function (done) { 606 | apply(target, JSON.parse(JSON.stringify(brightV9))) 607 | .then(function (map) { 608 | // add another layer that has no 'mapbox-layers' set 609 | map.addLayer(new VectorTileLayer()); 610 | const layer = JSON.parse( 611 | JSON.stringify(getMapboxLayer(map, 'landuse_park')), 612 | ); 613 | layer.paint['fill-color'] = 'red'; 614 | updateMapboxLayer(map, layer); 615 | const getStyle = getLayer(map, 'landuse_park').getStyle(); 616 | const feature = new Feature({ 617 | geometry: new Polygon([ 618 | [ 619 | [0, 0], 620 | [0, 1], 621 | [1, 1], 622 | [1, 0], 623 | [0, 0], 624 | ], 625 | ]), 626 | 'mvt:layer': 'landuse', 627 | class: 'park', 628 | }); 629 | const styles = getStyle(feature, 1); 630 | should(styles[0].getFill().getColor()).eql('rgba(255,0,0,1)'); 631 | done(); 632 | }) 633 | .catch((err) => done(err)); 634 | }); 635 | }); 636 | 637 | describe('removeMapboxLayer', function () { 638 | let target; 639 | beforeEach(function () { 640 | target = document.createElement('div'); 641 | }); 642 | 643 | it('removes a mapbox layer', function (done) { 644 | apply(target, JSON.parse(JSON.stringify(brightV9))) 645 | .then(function (map) { 646 | const layer = getLayer(map, 'landuse_park'); 647 | const oldRevision = layer.getRevision(); 648 | const mapboxLayer = getMapboxLayer(map, 'landuse_park'); 649 | removeMapboxLayer(map, mapboxLayer); 650 | should.equal(getMapboxLayer(map, 'landuse_park'), undefined); 651 | should.equal(layer.get('mapbox-layers').indexOf('landuse_park'), -1); 652 | should.equal(layer.getRevision(), oldRevision + 1); 653 | done(); 654 | }) 655 | .catch((err) => done(err)); 656 | }); 657 | }); 658 | 659 | describe('add-update-remove Mapbox layer', function () { 660 | let target; 661 | beforeEach(function () { 662 | target = document.createElement('div'); 663 | }); 664 | it('adds, updates and removes a mapbox layer', function (done) { 665 | apply(target, JSON.parse(JSON.stringify(brightV9))) 666 | .then(function (map) { 667 | addMapboxLayer( 668 | map, 669 | { 670 | id: 'inserted', 671 | source: 'mapbox', 672 | type: 'fill', 673 | filter: ['==', 'class', 'inserted'], 674 | paint: { 675 | 'fill-color': 'red', 676 | }, 677 | }, 678 | 'landuse_park', 679 | ); 680 | should(getLayer(map, 'inserted')).eql(getLayer(map, 'landuse_park')); 681 | let getStyle = getLayer(map, 'inserted').getStyle(); 682 | const feature = new Feature({ 683 | geometry: new Polygon([ 684 | [ 685 | [0, 0], 686 | [0, 1], 687 | [1, 1], 688 | [1, 0], 689 | [0, 0], 690 | ], 691 | ]), 692 | class: 'inserted', 693 | }); 694 | let styles = getStyle(feature, 1); 695 | should(styles[0].getFill().getColor()).eql('rgba(255,0,0,1)'); 696 | 697 | const inserted = getMapboxLayer(map, 'inserted'); 698 | inserted.paint['fill-color'] = 'blue'; 699 | updateMapboxLayer(map, inserted); 700 | getStyle = getLayer(map, 'inserted').getStyle(); 701 | styles = getStyle(feature, 1); 702 | should(styles[0].getFill().getColor()).eql('rgba(0,0,255,1)'); 703 | removeMapboxLayer(map, inserted); 704 | should(getLayer(map, 'inserted')).eql(undefined); 705 | done(); 706 | }) 707 | .catch((err) => done(err)); 708 | }); 709 | }); 710 | 711 | describe('manageVisibility', function () { 712 | let target; 713 | beforeEach(function () { 714 | target = document.createElement('div'); 715 | }); 716 | 717 | it('manages layer visibility', function (done) { 718 | apply(target, JSON.parse(JSON.stringify(brightV9))) 719 | .then(function (map) { 720 | const layer = getLayer(map, 'landuse_park'); 721 | should.equal(layer.getVisible(), true); 722 | 723 | const landuseParkLayer = getMapboxLayer(map, 'landuse_park'); 724 | const mapboxSource = landuseParkLayer.source; 725 | const mapboxLayers = map 726 | .get('mapbox-style') 727 | .layers.filter((layer) => layer.source == mapboxSource); 728 | mapboxLayers.forEach((mapboxLayer) => { 729 | mapboxLayer.layout = Object.assign(mapboxLayer.layout || {}, { 730 | visibility: 'none', 731 | }); 732 | updateMapboxLayer(map, mapboxLayer); 733 | }); 734 | should.equal(layer.getVisible(), false); 735 | landuseParkLayer.layout.visibility = 'visible'; 736 | updateMapboxLayer(map, landuseParkLayer); 737 | should.equal(layer.getVisible(), true); 738 | done(); 739 | }) 740 | .catch((err) => done(err)); 741 | }); 742 | }); 743 | }); 744 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "es2015", 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | "importHelpers": false, 11 | "strict": false, 12 | "strictNullChecks": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": false, 15 | "inlineSources": false, 16 | "skipLibCheck": true 17 | }, 18 | "include": [ 19 | "./src/**/*.js" 20 | ], 21 | "exclude": [] 22 | } -------------------------------------------------------------------------------- /tsconfig-typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "checkJs": true, 6 | "target": "ES5", 7 | "moduleResolution": "node", 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "allowSyntheticDefaultImports": true, 12 | "baseUrl": "./", 13 | "lib": [ 14 | "es2015", 15 | "dom", 16 | "webworker" 17 | ] 18 | }, 19 | "include": [ 20 | "src/**/*.js" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "checkJs": true, 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "es2015", 10 | "dom", 11 | "webworker" 12 | ] 13 | }, 14 | "include": [ 15 | "src/**/*.js", 16 | "*.cjs" 17 | ] 18 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "headings": { 3 | "readme": true, 4 | "document": true 5 | }, 6 | "sidebarLinks": { 7 | "Examples": "https://openlayers.org/ol-mapbox-style/examples/" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.examples.cjs: -------------------------------------------------------------------------------- 1 | const {readdirSync} = require('fs'); 2 | const {join, resolve} = require('path'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | /** 8 | * Get the list of examples from the examples directory. 9 | * 10 | * @param {string} dirName Name of the directory to read. 11 | * @param {Function} callback Function to execute for each example. 12 | * 13 | * @return {Object} Entries. 14 | */ 15 | function getExamples(dirName, callback) { 16 | const example_files = readdirSync(dirName); 17 | const entries = {}; 18 | 19 | // iterate through the list of files in the directory. 20 | for (const filename of example_files) { 21 | // ooo, javascript file! 22 | if (filename.endsWith('.js')) { 23 | // trim the entry name down to the file without the extension. 24 | const entry_name = filename.split('.')[0]; 25 | callback(entry_name, join(dirName, filename)); 26 | } 27 | } 28 | 29 | return entries; 30 | } 31 | 32 | /** 33 | * Creates an object with the entry names and file names 34 | * to be transformed. 35 | * 36 | * @param {string} dirName Name of the directory to read. 37 | * 38 | * @return {Object} with webpack entry points. 39 | */ 40 | function getEntries(dirName) { 41 | const entries = {}; 42 | getExamples(dirName, (entryName, filename) => { 43 | entries[entryName] = filename; 44 | }); 45 | return entries; 46 | } 47 | 48 | /** 49 | * Each example needs a dedicated HTML file. 50 | * This will create a "plugin" that outputs HTML from a template. 51 | * 52 | * @param {string} dirName Name of the directory to read. 53 | * 54 | * @return {Array} specifying webpack plugins. 55 | */ 56 | function getHtmlTemplates(dirName) { 57 | const html_conf = []; 58 | // create the array of HTML plugins. 59 | const template = join(dirName, '_template.html'); 60 | getExamples(dirName, (entryName, filename) => { 61 | html_conf.push( 62 | new HtmlWebpackPlugin({ 63 | title: entryName, 64 | // ensure each output has a unique filename 65 | filename: entryName + '.html', 66 | template, 67 | // without specifying chunks, all chunks are 68 | // included with the file. 69 | chunks: ['common', entryName], 70 | }), 71 | ); 72 | }); 73 | return html_conf; 74 | } 75 | 76 | module.exports = (env, argv) => { 77 | const prod = argv.mode === 'production'; 78 | return { 79 | context: __dirname, 80 | target: 'web', 81 | mode: prod ? 'production' : 'development', 82 | entry: getEntries(resolve(join(__dirname, 'examples'))), 83 | optimization: { 84 | runtimeChunk: { 85 | name: 'common', 86 | }, 87 | splitChunks: { 88 | name: 'common', 89 | chunks: 'initial', 90 | minChunks: 2, 91 | }, 92 | }, 93 | output: { 94 | filename: '[name].js', 95 | path: join(__dirname, '_site', 'examples'), 96 | publicPath: 'auto', 97 | }, 98 | resolve: { 99 | alias: { 100 | 'ol-mapbox-style': join(__dirname, 'src'), 101 | }, 102 | fallback: { 103 | 'assert': join(__dirname, 'node_modules', 'nanoassert'), 104 | }, 105 | }, 106 | devtool: 'source-map', 107 | module: { 108 | rules: [ 109 | { 110 | test: /\.css$/, 111 | use: [ 112 | prod ? MiniCssExtractPlugin.loader : 'style-loader', 113 | 'css-loader', 114 | ], 115 | }, 116 | ], 117 | }, 118 | plugins: [ 119 | new MiniCssExtractPlugin({ 120 | // Options similar to the same options in webpackOptions.output 121 | // both options are optional 122 | filename: '[name].css', 123 | chunkFilename: '[id].css', 124 | }), 125 | // ensure the data is copied over. 126 | // currently the index.html is manually created. 127 | // @ts-ignore 128 | new CopyWebpackPlugin({ 129 | patterns: [ 130 | { 131 | from: resolve(__dirname, './examples/data'), 132 | to: 'data', 133 | }, 134 | { 135 | from: resolve(__dirname, './examples/index.html'), 136 | to: 'index.html', 137 | }, 138 | ], 139 | }), 140 | ].concat(getHtmlTemplates('./examples')), 141 | }; 142 | }; 143 | --------------------------------------------------------------------------------