From 6d5bec642627f1caa49bd4d97b4cf312894caa80 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 27 Oct 2023 23:27:55 +0200 Subject: [PATCH] New 'updateMapboxSource()' utility function --- README.md | 24 ++++++++ src/apply.js | 137 +++++++++++++++++++++++++++++++++++----------- src/index.js | 1 + test/util.test.js | 109 +++++++++++++++++++++++++++++++++++- 4 files changed, 237 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index d3cd8995..9cc9a90b 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ and open a browser on the host and port indicated in the console output (usually - [setFeatureState](#setFeatureState) - [stylefunction](#stylefunction) - [updateMapboxLayer](#updateMapboxLayer) +- [updateMapboxSource](#updateMapboxSource) ### References @@ -616,6 +617,29 @@ Update a Mapbox Layer object in the style. The map will be re-rendered with the `void` +* * * + +#### updateMapboxSource + +▸ **updateMapboxSource**(`mapOrGroup`, `id`, `mapboxSource`): `Promise`<`Source`> + +Updates a Mapbox source object in the style. The according OpenLayers source will be replaced +and the map will be re-rendered. + +##### Parameters + +| Name | Type | Description | +| :------------- | :-------------------- | :------------------------------------------------- | +| `mapOrGroup` | `Map` \| `LayerGroup` | The Map or LayerGroup `apply` was called on. | +| `id` | `string` | Key of the source in the `sources` object literal. | +| `mapboxSource` | `any` | Mapbox source object. | + +##### Returns + +`Promise`<`Source`> + +Promise that resolves when the source has been updated. + ## Class: MapboxVectorLayer diff --git a/src/apply.js b/src/apply.js index 8b2ca648..5813c40f 100644 --- a/src/apply.js +++ b/src/apply.js @@ -652,43 +652,55 @@ function getBboxTemplate(projection) { return `{bbox-${projCode.toLowerCase().replace(/[^a-z0-9]/g, '-')}}`; } -function setupRasterLayer(glSource, styleUrl, options) { - const layer = new TileLayer(); - getTileJson(glSource, styleUrl, options) - .then(function ({tileJson, tileLoadFunction}) { - const source = new TileJSON({ - interpolate: - options.interpolate === undefined ? true : options.interpolate, - transition: 0, - crossOrigin: 'anonymous', - tileJSON: tileJson, - }); - source.tileGrid = sourceOptionsFromTileJSON( - glSource, - tileJson, - options - ).tileGrid; - if (options.projection) { - //@ts-ignore - source.projection = getProjection(options.projection); - } - const getTileUrl = source.getTileUrlFunction(); - if (tileLoadFunction) { - source.setTileLoadFunction(tileLoadFunction); - } - source.setTileUrlFunction(function (tileCoord, pixelRatio, projection) { - const bboxTemplate = getBboxTemplate(projection); - let src = getTileUrl(tileCoord, pixelRatio, projection); - if (src.indexOf(bboxTemplate) != -1) { - const bbox = source.getTileGrid().getTileCoordExtent(tileCoord); - src = src.replace(bboxTemplate, bbox.toString()); +function setupRasterSource(glSource, styleUrl, options) { + return new Promise(function (resolve, reject) { + getTileJson(glSource, styleUrl, options) + .then(function ({tileJson, tileLoadFunction}) { + const source = new TileJSON({ + interpolate: + options.interpolate === undefined ? true : options.interpolate, + transition: 0, + crossOrigin: 'anonymous', + tileJSON: tileJson, + }); + source.tileGrid = sourceOptionsFromTileJSON( + glSource, + tileJson, + options + ).tileGrid; + if (options.projection) { + //@ts-ignore + source.projection = getProjection(options.projection); } - return src; + const getTileUrl = source.getTileUrlFunction(); + if (tileLoadFunction) { + source.setTileLoadFunction(tileLoadFunction); + } + source.setTileUrlFunction(function (tileCoord, pixelRatio, projection) { + const bboxTemplate = getBboxTemplate(projection); + let src = getTileUrl(tileCoord, pixelRatio, projection); + if (src.indexOf(bboxTemplate) != -1) { + const bbox = source.getTileGrid().getTileCoordExtent(tileCoord); + src = src.replace(bboxTemplate, bbox.toString()); + } + return src; + }); + source.set('mapbox-source', glSource); + resolve(source); + }) + .catch(function (error) { + reject(error); }); - source.set('mapbox-source', glSource); + }); +} + +function setupRasterLayer(glSource, styleUrl, options) { + const layer = new TileLayer(); + setupRasterSource(glSource, styleUrl, options) + .then(function (source) { layer.setSource(source); }) - .catch(function (error) { + .catch(function () { layer.setSource(undefined); }); return layer; @@ -1407,6 +1419,65 @@ export function updateMapboxLayer(mapOrGroup, mapboxLayer) { } } +/** + * Updates a Mapbox source object in the style. The according OpenLayers source will be replaced + * and the map will be re-rendered. + * @param {Map|LayerGroup} mapOrGroup The Map or LayerGroup `apply` was called on. + * @param {string} id Key of the source in the `sources` object literal. + * @param {Object} mapboxSource Mapbox source object. + * @return {Promise} Promise that resolves when the source has been updated. + */ +export function updateMapboxSource(mapOrGroup, id, mapboxSource) { + const currentSource = getSource(mapOrGroup, id); + const layers = /** @type {Array} */ ( + mapOrGroup + .getLayers() + .getArray() + .filter(function (layer) { + return ( + (layer instanceof VectorLayer || + layer instanceof TileLayer || + layer instanceof VectorTileLayer) && + layer.getSource() === currentSource + ); + }) + ); + const metadata = mapOrGroup.get('mapbox-metadata'); + let newSourcePromise; + switch (mapboxSource.type) { + case 'vector': + newSourcePromise = setupVectorSource( + mapboxSource, + metadata.styleUrl, + metadata.options + ); + break; + case 'geojson': + newSourcePromise = Promise.resolve( + setupGeoJSONSource(mapboxSource, metadata.styleUrl, metadata.options) + ); + break; + case 'raster': + case 'raster-dem': + newSourcePromise = setupRasterSource( + mapboxSource, + metadata.styleUrl, + metadata.options + ); + break; + default: + return Promise.reject( + new Error('Unsupported source type ' + mapboxSource.type) + ); + } + newSourcePromise.then(function (newSource) { + layers.forEach(function (layer) { + layer.setSource(newSource); + }); + }); + return newSourcePromise; +} + /** * Remove a Mapbox Layer object from the style. The map will be re-rendered. * @param {Map|LayerGroup} mapOrGroup The Map or LayerGroup `apply` was called on. diff --git a/src/index.js b/src/index.js index c653b36f..9bb8a0b4 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ export { getLayers, getSource, getMapboxLayer, + updateMapboxSource, updateMapboxLayer, addMapboxLayer, removeMapboxLayer, diff --git a/test/util.test.js b/test/util.test.js index d300898f..7d7acb83 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -16,6 +16,7 @@ import { removeMapboxLayer, setupVectorSource, updateMapboxLayer, + updateMapboxSource, } from '../src/apply.js'; import {fetchResource} from '../src/util.js'; @@ -434,13 +435,119 @@ describe('util', function () { }); }); + describe('updateMapboxSource', function () { + let map, target, source1, source2, source3; + beforeEach(function () { + target = document.createElement('div'); + map = new Map({target: target}); + source1 = { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: {type: 'Point', coordinates: [1, 1]}, + }, + ], + }, + }; + source2 = { + type: 'vector', + tiles: ['http://example.com/{z}/{x}/{y}.pbf'], + }; + source3 = { + type: 'raster', + tiles: ['http://example.com/{z}/{x}/{y}.png'], + }; + return apply(map, { + version: 8, + sources: { + source1: source1, + source2: source2, + source3: source3, + }, + layers: [ + { + id: 'layer1', + source: 'source1', + type: 'circle', + }, + { + id: 'layer2', + source: 'source2', + 'source-layer': 'layer2', + type: 'circle', + }, + { + id: 'layer3', + source: 'source3', + type: 'raster', + }, + ], + }); + }); + it('updates a geojson source', function (done) { + should(getSource(map, 'source1').getFeatures()[0].get('modified')).eql( + undefined + ); + source1.data.features[0].properties.modified = true; + updateMapboxSource(map, 'source1', source1).then(function () { + try { + const source = getSource(map, 'source1'); + should(source).eql(getLayer(map, 'layer1').getSource()); + should(source.getFeatures()[0].get('modified')).eql(true); + should(); + done(); + } catch (err) { + done(err); + } + }); + }); + it('updates a vector source', function (done) { + should(getSource(map, 'source2').getUrls()[0]).eql( + 'http://example.com/{z}/{x}/{y}.pbf' + ); + source2.tiles[0] = 'http://example.com/{z}/{x}/{y}.mvt'; + updateMapboxSource(map, 'source2', source2).then(function () { + try { + const source = getSource(map, 'source2'); + should(source).eql(getLayer(map, 'layer2').getSource()); + should(source.getUrls()[0]).eql('http://example.com/{z}/{x}/{y}.mvt'); + done(); + } catch (err) { + done(err); + } + }); + }); + it('updates a raster source', function (done) { + should(getSource(map, 'source3').getTileUrlFunction()([0, 0, 0])).eql( + 'http://example.com/0/0/0.png' + ); + source3.tiles[0] = 'http://example.com/{z}/{x}/{y}.jpg'; + updateMapboxSource(map, 'source3', source3).then(function () { + try { + const source = getSource(map, 'source3'); + should(source).eql(getLayer(map, 'layer3').getSource()); + should(source.getTileUrlFunction()([0, 0, 0])).eql( + 'http://example.com/0/0/0.jpg' + ); + done(); + } catch (err) { + done(err); + } + }); + }); + }); + describe('updateMapboxLayer', function () { let target; beforeEach(function () { target = document.createElement('div'); }); - it('updates a mapbox layer', function (done) { + it('updates a geojson source', function (done) { apply(target, JSON.parse(JSON.stringify(brightV9))) .then(function (map) { // add another layer that has no 'mapbox-layers' set