}
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