Geomap: Add a MapLibre style base layer (#109841)
* GeoMap: Add maplibre style basemap * Docs: Add maplibre GeoMap base map * GeoMap: Add test dashboard to devenv * Add support for opacity * Simplify layer name and description * Add no repeat support * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Fix vale issues * Move to consistent async await approach * Increment ol-ext * Remove mapbox temporarily * Add mapbox back * Add access token url to docs * Highlight access token as public * Make capitalization consistent * Update yarn lock * Grab yarn lock from main * Add missing line to yarn lock * Fix last line again * Remove gdev dashboard for now --------- Co-authored-by: Remo Geissbühler <37411349+remogeissbuehler@users.noreply.github.com> Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
co-authored by
Isabel Matwawana
Remo Geissbühler
parent
3965761168
commit
f61578a50f
@@ -212,7 +212,7 @@ You can add multiple layers of data to a single geomap in order to create rich,
|
||||
|
||||
#### Layer type
|
||||
|
||||
There are seven map layer types to choose from in a geomap.
|
||||
There are eight map layer types to choose from in a geomap.
|
||||
|
||||
- [Markers](#markers-layer) renders a marker at each data point.
|
||||
- [Heatmap](#heatmap-layer) visualizes a heatmap of the data.
|
||||
@@ -225,6 +225,7 @@ There are seven map layer types to choose from in a geomap.
|
||||
- [CARTO basemap](#carto-basemap-layer) adds a layer from CARTO Raster basemaps.
|
||||
- [ArcGIS MapServer](#arcgis-mapserver-layer) adds a layer from an ESRI ArcGIS MapServer.
|
||||
- [XYZ Tile layer](#xyz-tile-layer) adds a map from a generic tile layer.
|
||||
- [MapLibre Style layer](#maplibre-style-layer) adds a map from a MapLibre/Mapbox style URL.
|
||||
|
||||
There are also two experimental (or alpha) layer types.
|
||||
|
||||
@@ -518,6 +519,13 @@ The XYZ Tile layer is a map from a generic tile layer.
|
||||
- [Tiled Web Map Wikipedia](https://en.wikipedia.org/wiki/Tiled_web_map)
|
||||
- [List of Open Street Map Tile Servers](https://wiki.openstreetmap.org/wiki/Tile_servers)
|
||||
|
||||
#### MapLibre Style layer
|
||||
|
||||
The MapLibre Style Layer is a map defined using a MapLibre/Mapbox `style.json` URL. The style contains the URL to the tiles, layer definitions, and more. Typically, they're based on vector tiles as opposed to raster tiles.
|
||||
|
||||
- **URL template** - Set a valid style URL. For example: `https://demotiles.maplibre.org/style.json`
|
||||
- **Access Token** - An API token for mapbox maps. Only works for `mapbox://` URLs. Refer to [mapbox access tokens documentation](https://docs.mapbox.com/help/dive-deeper/access-tokens/) for more information. In other cases, you might have to include the token in the URL. For example: `https://example.com/map/style.json?key=XXX`.
|
||||
|
||||
### Basemap layer options
|
||||
|
||||
A basemap layer provides the visual foundation for a mapping application. It typically contains data with global coverage. Several base layer options
|
||||
@@ -525,12 +533,13 @@ are available each with specific configuration options to style the base map.
|
||||
|
||||
Basemap layer types can also be added as layers. You can specify an opacity.
|
||||
|
||||
There are four basemap layer types to choose from in a geomap.
|
||||
There are five basemap layer types to choose from in a geomap.
|
||||
|
||||
- [Open Street Map](#open-street-map-layer) adds a map from a collaborative free geographic world database.
|
||||
- [CARTO basemap](#carto-basemap-layer) adds a layer from CARTO Raster basemaps.
|
||||
- [ArcGIS MapServer](#arcgis-mapserver-layer) adds a layer from an ESRI ArcGIS MapServer.
|
||||
- [XYZ Tile layer](#xyz-tile-layer) adds a map from a generic tile layer.
|
||||
- [MapLibre Style layer](#maplibre-style-layer) adds a map from a MapLibre/Mapbox style URL.
|
||||
|
||||
The default basemap layer uses the CARTO map. You can define custom default base layers in the `.ini` configuration file.
|
||||
|
||||
@@ -540,7 +549,7 @@ The default basemap layer uses the CARTO map. You can define custom default base
|
||||
|
||||
You can configure the default base map using config files with Grafana’s provisioning system. For more information on all the settings, refer to the [provisioning docs page](ref:provisioning-docs-page).
|
||||
|
||||
Use the JSON configuration option `default_baselayer_config` to define the default base map. There are currently four base map options to choose from: `carto`, `esri-xyz`, `osm-standard`, `xyz`. Here are some provisioning examples for each base map option.
|
||||
Use the JSON configuration option `default_baselayer_config` to define the default base map. There are currently five base map options to choose from: `carto`, `esri-xyz`, `osm-standard`, `xyz`, `maplibre`. Here are some provisioning examples for each base map option.
|
||||
|
||||
- **carto** loads the CartoDB tile server. You can choose from `auto`, `dark`, and `light` theme for the base map and can be set as shown below. The `showLabels` tag determines whether or not Grafana shows the Country details on top of the map. Here is an example:
|
||||
|
||||
@@ -606,6 +615,17 @@ default_baselayer_config = `{
|
||||
}`
|
||||
```
|
||||
|
||||
- **maplibre** loads a custom tile server defined by the user. Set a valid style `url` for this option to properly load a default base map. Here's an example:
|
||||
|
||||
```ini
|
||||
default_baselayer_config = `{
|
||||
"type": "maplibre",
|
||||
"config": {
|
||||
"url": "https://demotiles.maplibre.org/style.json"
|
||||
}
|
||||
}`
|
||||
```
|
||||
|
||||
`enable_custom_baselayers` allows you to enable or disable custom open source base maps that are already implemented. The default is `true`.
|
||||
|
||||
### Map controls options
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cartoLayers } from './carto';
|
||||
import { esriLayers } from './esri';
|
||||
import { genericLayers } from './generic';
|
||||
import { maplibreLayers } from './maplibre';
|
||||
import { osmLayers } from './osm';
|
||||
|
||||
/**
|
||||
@@ -11,4 +12,5 @@ export const basemapLayers = [
|
||||
...cartoLayers,
|
||||
...esriLayers, // keep formatting
|
||||
...genericLayers,
|
||||
...maplibreLayers,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import Map from 'ol/Map';
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import { apply } from 'ol-mapbox-style';
|
||||
|
||||
import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2, EventBus } from '@grafana/data';
|
||||
|
||||
// MapLibre Style Specification constants
|
||||
const LAYER_TYPE_BACKGROUND = 'background';
|
||||
const PAINT_BACKGROUND_OPACITY = 'background-opacity';
|
||||
|
||||
export interface MaplibreConfig {
|
||||
url: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
const sampleURL = 'https://tiles.stadiamaps.com/styles/alidade_smooth.json';
|
||||
|
||||
export const defaultMaplibreConfig: MaplibreConfig = {
|
||||
url: sampleURL,
|
||||
};
|
||||
|
||||
interface ExtendedMapLayerOptions<T> extends MapLayerOptions<T> {
|
||||
noRepeat?: boolean;
|
||||
}
|
||||
|
||||
export const maplibreLayer: MapLayerRegistryItem<MaplibreConfig> = {
|
||||
id: 'maplibre',
|
||||
name: 'MapLibre layer',
|
||||
description: 'Add layer using MapLibre style.json URL',
|
||||
isBaseMap: true,
|
||||
|
||||
create: async (
|
||||
map: Map,
|
||||
options: ExtendedMapLayerOptions<MaplibreConfig>,
|
||||
eventBus: EventBus,
|
||||
theme: GrafanaTheme2
|
||||
) => ({
|
||||
init: () => {
|
||||
const cfg = { ...options.config };
|
||||
if (!cfg.url) {
|
||||
cfg.url = defaultMaplibreConfig.url;
|
||||
}
|
||||
const layerOpacity = options.opacity ?? 1;
|
||||
const noRepeat = options.noRepeat ?? false;
|
||||
const layer = new LayerGroup({
|
||||
opacity: layerOpacity,
|
||||
});
|
||||
|
||||
const applyNoRepeat = () => {
|
||||
if (noRepeat) {
|
||||
// Set wrapX: false on the first layer source to prevent world repetition
|
||||
const firstLayer = layer.getLayers().item(0);
|
||||
if (firstLayer && 'getSource' in firstLayer && typeof firstLayer.getSource === 'function') {
|
||||
const source = firstLayer.getSource();
|
||||
if (source && 'setWrapX' in source && typeof source.setWrapX === 'function') {
|
||||
source.setWrapX(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle async operations in the background
|
||||
const loadStyle = async () => {
|
||||
try {
|
||||
if (!cfg.url) {
|
||||
console.warn('No URL provided for MapLibre style, layer will be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(cfg.url);
|
||||
if (!res.ok) {
|
||||
console.warn(`Failed to load MapLibre style from ${cfg.url}: ${res.status} ${res.statusText}`);
|
||||
// Try fallback approach
|
||||
await tryFallbackApply();
|
||||
return;
|
||||
}
|
||||
|
||||
const style = await res.json();
|
||||
|
||||
// Adjust background opacity - let LayerGroup opacity handle everything else
|
||||
if (Array.isArray(style?.layers)) {
|
||||
for (const l of style.layers) {
|
||||
if (l && l.type === LAYER_TYPE_BACKGROUND) {
|
||||
l.paint = l.paint || {};
|
||||
l.paint[PAINT_BACKGROUND_OPACITY] = layerOpacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await apply(layer, style, { styleUrl: cfg.url, accessToken: cfg.accessToken });
|
||||
applyNoRepeat();
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse or apply MapLibre style JSON:', error);
|
||||
// Try fallback approach
|
||||
await tryFallbackApply();
|
||||
}
|
||||
};
|
||||
|
||||
const tryFallbackApply = async () => {
|
||||
try {
|
||||
if (!cfg.url) {
|
||||
console.warn('No URL available for MapLibre fallback, layer will be empty');
|
||||
return;
|
||||
}
|
||||
await apply(layer, cfg.url, { accessToken: cfg.accessToken });
|
||||
applyNoRepeat();
|
||||
} catch (fallbackError) {
|
||||
console.warn('Failed to load MapLibre style from both JSON and direct URL approaches:', fallbackError);
|
||||
}
|
||||
};
|
||||
|
||||
// Start loading the style asynchronously
|
||||
loadStyle();
|
||||
|
||||
return layer;
|
||||
},
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addTextInput({
|
||||
path: 'config.url',
|
||||
name: 'URL template',
|
||||
description: 'URL to the styles.json file.',
|
||||
settings: {
|
||||
placeholder: defaultMaplibreConfig.url,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'config.accessToken',
|
||||
name: 'Public access token',
|
||||
description: 'Public access token for mapbox:// urls',
|
||||
settings: {
|
||||
placeholder: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const maplibreLayers = [maplibreLayer];
|
||||
Reference in New Issue
Block a user