grafana/public/app/plugins/panel/geomap/GeomapPanel.tsx

299 lines
8.0 KiB
TypeScript

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%;
`,
}));