The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/panel/geomap/GeomapPanel.tsx

299 lines
8.0 KiB

import React, { Component } from 'react';
import { geomapLayerRegistry } from './layers/registry';
import { Map, View } from 'ol';
import Attribution from 'ol/control/Attribution';
import Zoom from 'ol/control/Zoom';
import ScaleLine from 'ol/control/ScaleLine';
import BaseLayer from 'ol/layer/Base';
import { defaults as interactionDefaults } from 'ol/interaction';
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
import { PanelData, MapLayerHandler, MapLayerOptions, PanelProps, GrafanaTheme } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
import { defaultGrafanaThemedMap } from './layers/basemaps';
import { centerPointRegistry, MapCenterID } from './view';
import { fromLonLat } from 'ol/proj';
import { Coordinate } from 'ol/coordinate';
import { css } from '@emotion/css';
import { stylesFactory } from '@grafana/ui';
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
import { DebugOverlay } from './components/DebugOverlay';
import { getGlobalStyles } from './globalStyles';
import { Global } from '@emotion/react';
interface MapLayerState {
config: MapLayerOptions;
handler: MapLayerHandler;
layer: BaseLayer; // used to add|remove
}
// Allows multiple panels to share the same view instance
let sharedView: View | undefined = undefined;
type Props = PanelProps<GeomapPanelOptions>;
export class GeomapPanel extends Component<Props> {
globalCSS = getGlobalStyles(config.theme2);
map?: Map;
basemap?: BaseLayer;
layers: MapLayerState[] = [];
mouseWheelZoom?: MouseWheelZoom;
style = getStyles(config.theme);
overlayProps: OverlayProps = {};
shouldComponentUpdate(nextProps: Props) {
if (!this.map) {
return true; // not yet initalized
}
// Check for resize
if (this.props.height !== nextProps.height || this.props.width !== nextProps.width) {
this.map.updateSize();
}
// External configuraiton changed
let layersChanged = false;
if (this.props.options !== nextProps.options) {
layersChanged = this.optionsChanged(nextProps.options);
}
// External data changed
if (layersChanged || this.props.data !== nextProps.data) {
this.dataChanged(nextProps.data, nextProps.options.controls.showLegend);
}
return true; // always?
}
/**
* Called when the panel options change
*/
optionsChanged(options: GeomapPanelOptions): boolean {
let layersChanged = false;
const oldOptions = this.props.options;
console.log('options changed!', options);
if (options.view !== oldOptions.view) {
console.log('View changed');
this.map!.setView(this.initMapView(options.view));
}
if (options.controls !== oldOptions.controls) {
console.log('Crontrols changed');
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
}
if (options.basemap !== oldOptions.basemap) {
console.log('Basemap changed');
this.initBasemap(options.basemap);
layersChanged = true;
}
if (options.layers !== oldOptions.layers) {
console.log('layers changed');
this.initLayers(options.layers ?? []);
layersChanged = true;
}
return layersChanged;
}
/**
* Called when PanelData changes (query results etc)
*/
dataChanged(data: PanelData, showLegend?: boolean) {
const legends: React.ReactNode[] = [];
for (const state of this.layers) {
if (state.handler.update) {
state.handler.update(data);
}
if (showLegend && state.handler.legend) {
legends.push(state.handler.legend());
}
}
this.overlayProps.bottomLeft = legends;
}
initMapRef = (div: HTMLDivElement) => {
if (this.map) {
this.map.dispose();
}
if (!div) {
this.map = (undefined as unknown) as Map;
return;
}
const { options } = this.props;
this.map = new Map({
view: this.initMapView(options.view),
pixelRatio: 1, // or zoom?
layers: [], // loaded explicitly below
controls: [],
target: div,
interactions: interactionDefaults({
mouseWheelZoom: false, // managed by initControls
}),
});
this.mouseWheelZoom = new MouseWheelZoom();
this.map.addInteraction(this.mouseWheelZoom);
this.initControls(options.controls);
this.initBasemap(options.basemap);
this.initLayers(options.layers);
this.dataChanged(this.props.data, options.controls.showLegend);
this.forceUpdate(); // first render
};
initBasemap(cfg: MapLayerOptions) {
if (!this.map) {
return;
}
if (!cfg) {
cfg = { type: defaultGrafanaThemedMap.id };
}
const item = geomapLayerRegistry.getIfExists(cfg.type) ?? defaultGrafanaThemedMap;
const layer = item.create(this.map, cfg, config.theme2).init();
if (this.basemap) {
this.map.removeLayer(this.basemap);
this.basemap.dispose();
}
this.basemap = layer;
this.map.getLayers().insertAt(0, this.basemap);
}
initLayers(layers: MapLayerOptions[]) {
// 1st remove existing layers
for (const state of this.layers) {
this.map!.removeLayer(state.layer);
state.layer.dispose();
}
if (!layers) {
layers = [];
}
this.layers = [];
for (const overlay of layers) {
const item = geomapLayerRegistry.getIfExists(overlay.type);
if (!item) {
console.warn('unknown layer type: ', overlay);
continue; // TODO -- panel warning?
}
const handler = item.create(this.map!, overlay, config.theme2);
const layer = handler.init();
this.map!.addLayer(layer);
this.layers.push({
config: overlay,
layer,
handler,
});
}
}
initMapView(config: MapViewConfig): View {
let view = new View({
center: [0, 0],
zoom: 1,
});
// With shared views, all panels use the same view instance
if (config.shared) {
if (!sharedView) {
sharedView = view;
} else {
view = sharedView;
}
}
const v = centerPointRegistry.getIfExists(config.center.id);
if (v) {
let coord: Coordinate | undefined = undefined;
if (v.lat == null) {
if (v.id === MapCenterID.Coordinates) {
const center = config.center ?? {};
coord = [center.lon ?? 0, center.lat ?? 0];
} else {
console.log('TODO, view requires special handling', v);
}
} else {
coord = [v.lon ?? 0, v.lat ?? 0];
}
if (coord) {
view.setCenter(fromLonLat(coord));
}
}
if (config.maxZoom) {
view.setMaxZoom(config.maxZoom);
}
if (config.minZoom) {
view.setMaxZoom(config.minZoom);
}
if (config.zoom) {
view.setZoom(config.zoom);
}
return view;
}
initControls(options: ControlsOptions) {
if (!this.map) {
return;
}
this.map.getControls().clear();
if (options.showZoom) {
this.map.addControl(new Zoom());
}
if (options.showScale) {
this.map.addControl(
new ScaleLine({
units: options.scaleUnits,
minWidth: 100,
})
);
}
this.mouseWheelZoom!.setActive(Boolean(options.mouseWheelZoom));
if (options.showAttribution) {
this.map.addControl(new Attribution({ collapsed: true, collapsible: true }));
}
// Update the react overlays
const overlayProps: OverlayProps = {};
if (options.showDebug) {
overlayProps.topRight = [<DebugOverlay key="debug" map={this.map} />];
}
this.overlayProps = overlayProps;
}
render() {
return (
<>
<Global styles={this.globalCSS} />
<div className={this.style.wrap}>
<div className={this.style.map} ref={this.initMapRef}></div>
<GeomapOverlay {...this.overlayProps} />
</div>
</>
);
}
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
wrap: css`
position: relative;
width: 100%;
height: 100%;
`,
map: css`
position: absolute;
z-index: 0;
width: 100%;
height: 100%;
`,
}));