Cloud-masked Sentinel-2 imagery using bands from multiple Zarr groups.
The GeoZarr source supports loading bands from multiple groups within the same Zarr hierarchy. In this example, a true-color composite is rendered from B02 (blue), B03 (green), and B04 (red) reflectance values from the measurements/reflectance group. The cloud probability (cld) from the quality/probability group is used to fade out cloudy areas, revealing the OSM basemap underneath.
When bands come from different groups, each band is specified as an object with a name and a group path. The group is resolved relative to the url, which can be any common ancestor in the Zarr hierarchy — not necessarily the store root.
Since the cloud probability data is only available at 20 m resolution, it is resampled at higher zoom levels to match the 10 m reflectance bands.
import Map from 'ol/Map.js';
import {getView, withExtentCenter, withHigherResolutions} from 'ol/View.js';
import Link from 'ol/interaction/Link.js';
import TileLayer from 'ol/layer/WebGLTile.js';
import GeoZarr from 'ol/source/GeoZarr.js';
import OSM from 'ol/source/OSM.js';
const source = new GeoZarr({
url: 'https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260120T125339_N0511_R138_T27VWL_20260120T131151.zarr',
bands: [
{name: 'b04', group: 'measurements/reflectance'},
{name: 'b03', group: 'measurements/reflectance'},
{name: 'b02', group: 'measurements/reflectance'},
{name: 'cld', group: 'quality/probability'},
],
});
const layer = new TileLayer({
style: {
variables: {threshold: 50},
gamma: 1.5,
color: [
'color',
['interpolate', ['linear'], ['band', 1], 0, 0, 0.5, 255],
['interpolate', ['linear'], ['band', 2], 0, 0, 0.5, 255],
['interpolate', ['linear'], ['band', 3], 0, 0, 0.5, 255],
// Hide pixels whose cloud probability exceeds the threshold.
['case', ['>', ['band', 4], ['var', 'threshold']], 0, 1],
],
},
source,
});
const map = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
layer,
],
target: 'map',
view: getView(source, withHigherResolutions(2), withExtentCenter()),
});
map.addInteraction(new Link());
const thresholdSlider = document.getElementById('threshold');
const thresholdValue = document.getElementById('threshold-value');
thresholdSlider.addEventListener('input', function () {
thresholdValue.textContent = thresholdSlider.value;
layer.updateStyleVariables({threshold: parseFloat(thresholdSlider.value)});
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GeoZarr Multi-Group</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<div class="row mt-2">
<div class="col-auto">
<label class="form-label" for="threshold">Cloud threshold: <span id="threshold-value">50</span>%</label>
<input type="range" class="form-range" id="threshold" min="0" max="100" value="50">
</div>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
{
"name": "geozarr-groups",
"dependencies": {
"ol": "10.9.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}