diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md index 241622708db..6158f14c61a 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md @@ -473,6 +473,10 @@ A map from a collaborative free geographic world database. - **Opacity** from 0 (transparent) to 1 (opaque) - **Display tooltip** - allows you to toggle tooltips for the layer. +{{< admonition type="note" >}} +OpenStreetMap requires attribution by license. When you use this layer, attribution automatically displays in the map controls and cannot be hidden. +{{< /admonition >}} + [About Open Street Map](https://www.openstreetmap.org/about) #### CARTO basemap layer @@ -489,6 +493,10 @@ A CARTO layer is from CARTO Raster basemaps. - **Opacity** from 0 (transparent) to 1 (opaque) - **Display tooltip** - allows you to toggle tooltips for the layer. +{{< admonition type="note" >}} +CARTO requires attribution by license. When you use this layer, attribution automatically displays in the map controls and cannot be hidden. +{{< /admonition >}} + [About CARTO](https://carto.com/about-us/) #### ArcGIS MapServer layer @@ -654,6 +662,10 @@ Enables the mouse wheel to be used for zooming in or out. Displays attribution for basemap layers. +{{< admonition type="note" >}} +Attribution is required by license for certain map layers, including OpenStreetMap and CARTO. When you use these layers, their attribution automatically displays and cannot be hidden. The toggle becomes **Show optional attribution** and controls whether attribution from other layers is also displayed. +{{< /admonition >}} + {{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-attribution-9-1-0.png" max-width="400px" alt="Geomap panel attribution" >}} #### Show scale diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index ec522923e61..e9c52625cf4 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -67,6 +67,12 @@ export interface MapLayerRegistryItem extends Registr */ hideOpacity?: boolean; + /** + * Indicates that this layer requires attribution to be shown by license + * When true, the attribution control will always be displayed regardless of user settings + */ + requiresAttribution?: boolean; + /** * Function that configures transformation and returns a transformer * @param options diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index eb7d7c7dad4..c4389ec2b98 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -9,6 +9,7 @@ import Zoom from 'ol/control/Zoom'; import { Coordinate } from 'ol/coordinate'; import { isEmpty } from 'ol/extent'; import MouseWheelZoom from 'ol/interaction/MouseWheelZoom'; +import TileLayer from 'ol/layer/Tile'; import { fromLonLat, transformExtent } from 'ol/proj'; import { Component, ReactNode } from 'react'; import * as React from 'react'; @@ -30,7 +31,7 @@ import { MeasureVectorLayer } from './components/MeasureVectorLayer'; import { GeomapHoverPayload } from './event'; import { getGlobalStyles } from './globalStyles'; import { defaultMarkersConfig } from './layers/data/markersLayer'; -import { DEFAULT_BASEMAP_CONFIG } from './layers/registry'; +import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry'; import { ControlsOptions, Options, MapLayerState, MapViewConfig, TooltipMode } from './types'; import { getActions } from './utils/actions'; import { getLayersExtent } from './utils/getLayersExtent'; @@ -396,7 +397,45 @@ export class GeomapPanel extends Component { this.mouseWheelZoom?.setActive(Boolean(options.mouseWheelZoom)); - if (options.showAttribution) { + // Handle attribution visibility per layer based on required vs optional + let hasAnyAttribution = false; + + for (const layerState of this.layers) { + const layerType = layerState.options.type; + const layerRegistryItem = layerType ? geomapLayerRegistry.getIfExists(layerType) : null; + const requiresAttribution = layerRegistryItem?.requiresAttribution === true; + + // Check if this layer's source has attribution capability + const layer = layerState.layer; + if (layer instanceof TileLayer) { + const source = layer.getSource(); + if (source && typeof source.getAttributions === 'function') { + const currentAttributions = source.getAttributions(); + + // Store original attribution if not already stored + if (!layerState.originalAttribution && currentAttributions) { + layerState.originalAttribution = currentAttributions; + } + + // Show attribution if it's required OR if user has enabled optional attributions + if (requiresAttribution || options.showAttribution) { + // Restore original attribution if we had cleared it + if (layerState.originalAttribution && typeof source.setAttributions === 'function') { + source.setAttributions(layerState.originalAttribution); + } + hasAnyAttribution = true; + } else { + // Hide optional attribution by clearing it + if (typeof source.setAttributions === 'function') { + source.setAttributions(null); + } + } + } + } + } + + // Add attribution control if there are any attributions to show + if (hasAnyAttribution) { this.map.addControl(new Attribution({ collapsed: true, collapsible: true })); } diff --git a/public/app/plugins/panel/geomap/layers/basemaps/carto.ts b/public/app/plugins/panel/geomap/layers/basemaps/carto.ts index 3e2572a702f..569b5ff6dac 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/carto.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/carto.ts @@ -27,6 +27,7 @@ export const carto: MapLayerRegistryItem = { name: 'CARTO basemap', description: 'Add layer CARTO Raster basemaps', isBaseMap: true, + requiresAttribution: true, defaultOptions: defaultCartoConfig, /** diff --git a/public/app/plugins/panel/geomap/layers/basemaps/osm.ts b/public/app/plugins/panel/geomap/layers/basemaps/osm.ts index 078ddafff43..d12b4ecef46 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/osm.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/osm.ts @@ -9,6 +9,7 @@ export const standard: MapLayerRegistryItem = { name: 'Open Street Map', description: 'Add map from a collaborative free geographic world database', isBaseMap: true, + requiresAttribution: true, /** * Function that configures transformation and returns a transformer diff --git a/public/app/plugins/panel/geomap/layers/registry.ts b/public/app/plugins/panel/geomap/layers/registry.ts index 15e6fbb7861..b8b23bc184c 100644 --- a/public/app/plugins/panel/geomap/layers/registry.ts +++ b/public/app/plugins/panel/geomap/layers/registry.ts @@ -26,6 +26,8 @@ export const defaultBaseLayer: MapLayerRegistryItem = { id: DEFAULT_BASEMAP_CONFIG.type, name: 'Default base layer', isBaseMap: true, + // Default uses CARTO which requires attribution + requiresAttribution: true, create: (map: OpenLayersMap, options: MapLayerOptions, eventBus: EventBus, theme: GrafanaTheme2) => { const serverLayerType = config?.geomapDefaultBaseLayerConfig?.type; diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index 7573f9317aa..9e04b87a97a 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -7,6 +7,7 @@ import { GeomapPanel } from './GeomapPanel'; import { LayersEditor } from './editor/LayersEditor'; import { MapViewEditor } from './editor/MapViewEditor'; import { getLayerEditor } from './editor/layerEditor'; +import { geomapLayerRegistry } from './layers/registry'; import { mapPanelChangedHandler, mapMigrationHandler } from './migrations'; import { defaultMapViewConfig, Options, TooltipMode, GeomapInstanceState } from './types'; @@ -105,6 +106,17 @@ export const plugin = new PanelPlugin(GeomapPanel) // The controls section category = [t('geomap.category-map-controls', 'Map controls')]; + + // Check if any layer requires attribution + const requiresAttribution = state?.layers?.some((layerState) => { + const layerType = layerState.options.type; + if (layerType) { + const layerRegistryItem = geomapLayerRegistry.getIfExists(layerType); + return layerRegistryItem?.requiresAttribution === true; + } + return false; + }); + builder .addBooleanSwitch({ category, @@ -123,11 +135,15 @@ export const plugin = new PanelPlugin(GeomapPanel) .addBooleanSwitch({ category, path: 'controls.showAttribution', - name: t('geomap.name-show-attribution', 'Show attribution'), - description: t( - 'geomap.description-show-attribution', - 'Show the map source attribution info in the lower right' - ), + name: requiresAttribution + ? t('geomap.name-show-optional-attribution', 'Show optional attribution') + : t('geomap.name-show-attribution', 'Show attribution'), + description: requiresAttribution + ? t( + 'geomap.description-show-optional-attribution', + 'Required attributions are always shown. Toggle this to also show attribution from other layers at your discretion.' + ) + : t('geomap.description-show-attribution', 'Show the map source attribution info in the lower right'), defaultValue: true, }) .addBooleanSwitch({ diff --git a/public/app/plugins/panel/geomap/types.ts b/public/app/plugins/panel/geomap/types.ts index 2ef58f6e65e..5411965efee 100644 --- a/public/app/plugins/panel/geomap/types.ts +++ b/public/app/plugins/panel/geomap/types.ts @@ -51,6 +51,7 @@ export interface MapLayerState extends LayerElement { onChange: (cfg: MapLayerOptions) => void; isBasemap?: boolean; mouseEvents: Subject; + originalAttribution?: any; // Store original attribution to restore when showing optional attributions } export {