Skip to content

Commit

Permalink
Adding a MapboxLayer for creating a single layer from a Mapbox style
Browse files Browse the repository at this point in the history
  • Loading branch information
tschaub committed May 1, 2020
1 parent 9b8458f commit 6ce5f1f
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<h1>ol-mapbox-style Examples</h1>
<ul>
<li><a href="mapbox.html">Mapbox</a> (requires a <a href="https://www.mapbox.com/studio/account/tokens/">Mapbox API token</a>) &ndash; Vector tile map from Mapbox's Bright style</li>
<li><a href="mapbox-layer.html">MapboxLayer</a> (requires a <a href="https://www.mapbox.com/studio/account/tokens/">Mapbox API token</a>) &ndash; Using the MapboxLayer to add a single layer to an OpenLayers map based on a Mapbox style.</li>
<li><a href="openmaptiles.html">OpenMapTiles</a> (requires a <a href="https://cloud.maptiler.com/account/keys">Maptiler Cloud API token</a>) &ndash; Vector tile map from OpenMapTiles's basic style</li>
<li><a href="geojson.html">GeoJSON</a> &ndash; Map with a vector layer from GeoJSON</li>
<li><a href="geojson-inline.html">Inline GeoJSON</a> &ndash; Map with a vector layer from GeoJSON embedded in the style object</li>
Expand Down
25 changes: 25 additions & 0 deletions examples/mapbox-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import MapboxLayer from 'ol-mapbox-style/dist/MapboxLayer';


let key = document.cookie.replace(/(?:(?:^|.*;\s*)mapbox_access_token\s*\=\s*([^;]*).*$)|^.*$/, '$1');
if (!key) {
key = window.prompt('Enter your Mapbox API access token:');
document.cookie = 'mapbox_access_token=' + key + '; expires=Fri, 31 Dec 9999 23:59:59 GMT';
}

new Map({
target: 'map',
view: new View({
center: [0, 0],
zoom: 2
}),
layers: [
new MapboxLayer({
accessToken: key,
styleURL: 'mapbox://styles/mapbox/bright-v9'
})
]
});
149 changes: 149 additions & 0 deletions src/MapboxLayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {applyStyle} from './index';
import MVT from 'ol/format/MVT';
import SourceState from 'ol/source/State';
import VectorTileLayer from 'ol/layer/VectorTile';
import VectorTileSource from 'ol/source/VectorTile';

const mapboxBaseURL = 'https://api.mapbox.com';

function getMapboxPath(url) {
const startsWith = 'mapbox://';
if (url.indexOf(startsWith) !== 0) {
return '';
}
return url.slice(startsWith.length);
}

function normalizeSpriteURL(url, token) {
const mapboxPath = getMapboxPath(url);
if (!mapboxPath) {
return url;
}
const startsWith = 'sprites/';
if (mapboxPath.indexOf(startsWith) !== 0) {
throw new Error(`unexpected sprites url: ${url}`);
}
const sprite = mapboxPath.slice(startsWith.length);

return `${mapboxBaseURL}/styles/v1/${sprite}/sprite?access_token=${token}`;
}

function normalizeStyleURL(url, token) {
const mapboxPath = getMapboxPath(url);
if (!mapboxPath) {
return url;
}
const startsWith = 'styles/';
if (mapboxPath.indexOf(startsWith) !== 0) {
throw new Error(`unexpected style url: ${url}`);
}
const style = mapboxPath.slice(startsWith.length);

return `${mapboxBaseURL}/styles/v1/${style}?&access_token=${token}`;
}

function normalizeSourceURL(url, token) {
const mapboxPath = getMapboxPath(url);
if (!mapboxPath) {
return url;
}
return `https://{a-d}.tiles.mapbox.com/v4/${mapboxPath}/{z}/{x}/{y}.vector.pbf?access_token=${token}`;
}

export default class MapboxLayer extends VectorTileLayer {
constructor(options) {
const superOptions = Object.assign({
declutter: true
}, options);

delete superOptions.styleURL;
delete superOptions.source;
delete superOptions.layers;
delete superOptions.accessToken;

superOptions.source = new VectorTileSource({
state: SourceState.LOADING,
format: new MVT()
});

super(superOptions);

this.sourceId = options.source;
this.layers = options.layers;
this.accessToken = options.accessToken;
this.fetchStyle(options.styleURL);
}

fetchStyle(styleURL) {
const url = normalizeStyleURL(styleURL, this.accessToken);
fetch(url).then(response => {
if (!response.ok) {
throw new Error(`unexpected response when fetching style: ${response.status}`);
}
return response.json();
}).then(style => {
this.onStyleLoad(style);
}).catch(error => {
this.handleError(error);
});
}

onStyleLoad(style) {
let sourceId;
let sourceIdOrLayersList;
if (this.layers) {
// confirm all layers share the same source
const lookup = {};
for (let i = 0; i < style.layers.length; ++i) {
const layer = style.layers[i];
if (layer.source) {
lookup[layer.id] = layer.source;
}
}
let firstSource;
for (let i = 0; i < this.layers.length; ++i) {
const candidate = lookup[this.layers[i]];
if (!candidate) {
this.handleError(new Error(`could not find source for ${this.layers[i]}`));
return;
}
if (!firstSource) {
firstSource = candidate;
} else if (firstSource !== candidate) {
this.handleError(new Error(`layers can only use a single source, found ${firstSource} and ${candidate}`));
return;
}
}
sourceId = firstSource;
sourceIdOrLayersList = this.layers;
} else {
sourceIdOrLayersList = this.sourceId;
}

if (!sourceIdOrLayersList) {
// default to the first source in the style
sourceId = Object.keys(style.sources)[0];
sourceIdOrLayersList = sourceId;
}

if (style.sprite) {
style.sprite = normalizeSpriteURL(style.sprite, this.accessToken);
}

const source = this.getSource();
source.setUrl(normalizeSourceURL(style.sources[sourceId].url, this.accessToken));

applyStyle(this, style, sourceIdOrLayersList).then(() => {
source.setState(SourceState.READY);
}).catch(error => {
this.handleError(error);
});
}

handleError(error) {
// TODO: make this error accessible to an error listener
console.error(error); // eslint-disable-line
const source = this.getSource();
source.setState(SourceState.ERROR);
}
}

0 comments on commit 6ce5f1f

Please sign in to comment.