2021-07-26 18:50:15 -04:00
|
|
|
import React, { Component, ReactNode } from 'react';
|
2021-07-21 13:48:20 -07:00
|
|
|
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry, defaultBaseLayer } from './layers/registry';
|
2021-07-28 18:34:42 -07:00
|
|
|
import { Map, MapBrowserEvent, View } from 'ol';
|
2021-07-09 08:53:07 -07:00
|
|
|
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';
|
|
|
|
|
|
2021-07-28 18:34:42 -07:00
|
|
|
import {
|
|
|
|
|
PanelData,
|
|
|
|
|
MapLayerHandler,
|
|
|
|
|
MapLayerOptions,
|
|
|
|
|
PanelProps,
|
|
|
|
|
GrafanaTheme,
|
|
|
|
|
DataHoverClearEvent,
|
|
|
|
|
DataHoverEvent,
|
|
|
|
|
DataFrame,
|
|
|
|
|
} from '@grafana/data';
|
2021-07-09 08:53:07 -07:00
|
|
|
import { config } from '@grafana/runtime';
|
|
|
|
|
|
|
|
|
|
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
|
|
|
|
|
import { centerPointRegistry, MapCenterID } from './view';
|
2021-07-28 18:34:42 -07:00
|
|
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
2021-07-09 08:53:07 -07:00
|
|
|
import { Coordinate } from 'ol/coordinate';
|
|
|
|
|
import { css } from '@emotion/css';
|
2021-10-01 11:06:11 -07:00
|
|
|
import { PanelContext, PanelContextRoot, Portal, stylesFactory, VizTooltipContainer } from '@grafana/ui';
|
2021-07-09 08:53:07 -07:00
|
|
|
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
|
|
|
|
|
import { DebugOverlay } from './components/DebugOverlay';
|
|
|
|
|
import { getGlobalStyles } from './globalStyles';
|
|
|
|
|
import { Global } from '@emotion/react';
|
2021-07-28 18:34:42 -07:00
|
|
|
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
|
|
|
|
|
import { DataHoverView } from './components/DataHoverView';
|
2021-07-09 08:53:07 -07:00
|
|
|
|
|
|
|
|
interface MapLayerState {
|
2021-07-15 12:00:19 -07:00
|
|
|
config: MapLayerOptions;
|
2021-07-09 08:53:07 -07:00
|
|
|
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>;
|
2021-07-28 18:34:42 -07:00
|
|
|
interface State extends OverlayProps {
|
|
|
|
|
ttip?: GeomapHoverPayload;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
export class GeomapPanel extends Component<Props, State> {
|
2021-10-01 11:06:11 -07:00
|
|
|
static contextType = PanelContextRoot;
|
|
|
|
|
panelContext: PanelContext = {} as PanelContext;
|
|
|
|
|
|
2021-07-09 08:53:07 -07:00
|
|
|
globalCSS = getGlobalStyles(config.theme2);
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
counter = 0;
|
2021-07-15 12:00:19 -07:00
|
|
|
map?: Map;
|
|
|
|
|
basemap?: BaseLayer;
|
2021-07-09 08:53:07 -07:00
|
|
|
layers: MapLayerState[] = [];
|
2021-07-15 12:00:19 -07:00
|
|
|
mouseWheelZoom?: MouseWheelZoom;
|
2021-07-09 08:53:07 -07:00
|
|
|
style = getStyles(config.theme);
|
2021-07-28 18:34:42 -07:00
|
|
|
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
|
|
|
|
|
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {};
|
|
|
|
|
}
|
2021-07-28 18:34:42 -07:00
|
|
|
|
2021-07-19 08:40:56 -07:00
|
|
|
componentDidMount() {
|
2021-10-01 11:06:11 -07:00
|
|
|
this.panelContext = this.context as PanelContext;
|
|
|
|
|
if (this.panelContext.onInstanceStateChange) {
|
|
|
|
|
this.panelContext.onInstanceStateChange(this);
|
|
|
|
|
}
|
2021-07-19 08:40:56 -07:00
|
|
|
}
|
|
|
|
|
|
2021-07-09 08:53:07 -07:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-20 21:09:08 -04:00
|
|
|
// External configuration changed
|
2021-07-09 08:53:07 -07:00
|
|
|
let layersChanged = false;
|
|
|
|
|
if (this.props.options !== nextProps.options) {
|
|
|
|
|
layersChanged = this.optionsChanged(nextProps.options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// External data changed
|
|
|
|
|
if (layersChanged || this.props.data !== nextProps.data) {
|
2021-07-26 18:50:15 -04:00
|
|
|
this.dataChanged(nextProps.data);
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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');
|
2021-07-15 12:00:19 -07:00
|
|
|
this.map!.setView(this.initMapView(options.view));
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.controls !== oldOptions.controls) {
|
2021-07-20 21:09:08 -04:00
|
|
|
console.log('Controls changed');
|
2021-07-09 08:53:07 -07:00
|
|
|
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');
|
2021-07-21 23:41:27 -07:00
|
|
|
this.initLayers(options.layers ?? []); // async
|
2021-07-09 08:53:07 -07:00
|
|
|
layersChanged = true;
|
|
|
|
|
}
|
|
|
|
|
return layersChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called when PanelData changes (query results etc)
|
|
|
|
|
*/
|
2021-07-26 18:50:15 -04:00
|
|
|
dataChanged(data: PanelData) {
|
2021-07-09 08:53:07 -07:00
|
|
|
for (const state of this.layers) {
|
|
|
|
|
if (state.handler.update) {
|
|
|
|
|
state.handler.update(data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-21 23:41:27 -07:00
|
|
|
initMapRef = async (div: HTMLDivElement) => {
|
2021-07-09 08:53:07 -07:00
|
|
|
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);
|
2021-07-26 18:50:15 -04:00
|
|
|
await this.initLayers(options.layers);
|
2021-07-09 08:53:07 -07:00
|
|
|
this.forceUpdate(); // first render
|
2021-07-28 18:34:42 -07:00
|
|
|
|
|
|
|
|
// Tooltip listener
|
|
|
|
|
this.map.on('pointermove', this.pointerMoveListener);
|
|
|
|
|
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
2021-10-12 05:57:17 -07:00
|
|
|
this.props.eventBus.publish(new DataHoverClearEvent());
|
2021-07-28 18:34:42 -07:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-09-10 09:05:03 -07:00
|
|
|
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
2021-07-28 18:34:42 -07:00
|
|
|
if (!this.map) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const mouse = evt.originalEvent as any;
|
|
|
|
|
const pixel = this.map.getEventPixel(mouse);
|
|
|
|
|
const hover = toLonLat(this.map.getCoordinateFromPixel(pixel));
|
|
|
|
|
|
|
|
|
|
const { hoverPayload } = this;
|
|
|
|
|
hoverPayload.pageX = mouse.pageX;
|
|
|
|
|
hoverPayload.pageY = mouse.pageY;
|
|
|
|
|
hoverPayload.point = {
|
|
|
|
|
lat: hover[1],
|
|
|
|
|
lon: hover[0],
|
|
|
|
|
};
|
|
|
|
|
hoverPayload.data = undefined;
|
|
|
|
|
hoverPayload.columnIndex = undefined;
|
|
|
|
|
hoverPayload.rowIndex = undefined;
|
2021-09-23 19:31:36 -07:00
|
|
|
hoverPayload.feature = undefined;
|
2021-07-28 18:34:42 -07:00
|
|
|
|
|
|
|
|
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
|
|
|
|
const features: GeomapHoverFeature[] = [];
|
|
|
|
|
this.map.forEachFeatureAtPixel(pixel, (feature, layer, geo) => {
|
|
|
|
|
if (!hoverPayload.data) {
|
|
|
|
|
const props = feature.getProperties();
|
|
|
|
|
const frame = props['frame'];
|
|
|
|
|
if (frame) {
|
|
|
|
|
hoverPayload.data = ttip.data = frame as DataFrame;
|
|
|
|
|
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
2021-09-23 19:31:36 -07:00
|
|
|
} else {
|
|
|
|
|
hoverPayload.feature = ttip.feature = feature;
|
2021-07-28 18:34:42 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
features.push({ feature, layer, geo });
|
|
|
|
|
});
|
|
|
|
|
this.hoverPayload.features = features.length ? features : undefined;
|
|
|
|
|
this.props.eventBus.publish(this.hoverEvent);
|
|
|
|
|
|
|
|
|
|
const currentTTip = this.state.ttip;
|
2021-09-23 19:31:36 -07:00
|
|
|
if (
|
|
|
|
|
ttip.data !== currentTTip?.data ||
|
|
|
|
|
ttip.rowIndex !== currentTTip?.rowIndex ||
|
|
|
|
|
ttip.feature !== currentTTip?.feature
|
|
|
|
|
) {
|
2021-07-28 18:34:42 -07:00
|
|
|
this.setState({ ttip: { ...hoverPayload } });
|
|
|
|
|
}
|
2021-07-09 08:53:07 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-21 23:41:27 -07:00
|
|
|
async initBasemap(cfg: MapLayerOptions) {
|
2021-07-15 12:00:19 -07:00
|
|
|
if (!this.map) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-07-21 13:48:20 -07:00
|
|
|
|
|
|
|
|
if (!cfg?.type || config.geomapDisableCustomBaseLayer) {
|
|
|
|
|
cfg = DEFAULT_BASEMAP_CONFIG;
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
2021-07-21 13:48:20 -07:00
|
|
|
const item = geomapLayerRegistry.getIfExists(cfg.type) ?? defaultBaseLayer;
|
2021-07-21 23:41:27 -07:00
|
|
|
const handler = await item.create(this.map, cfg, config.theme2);
|
|
|
|
|
const layer = handler.init();
|
2021-07-09 08:53:07 -07:00
|
|
|
if (this.basemap) {
|
|
|
|
|
this.map.removeLayer(this.basemap);
|
|
|
|
|
this.basemap.dispose();
|
|
|
|
|
}
|
|
|
|
|
this.basemap = layer;
|
2021-10-27 14:21:07 +01:00
|
|
|
this.map.getLayers().insertAt(0, this.basemap!);
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
async initLayers(layers: MapLayerOptions[]) {
|
2021-07-09 08:53:07 -07:00
|
|
|
// 1st remove existing layers
|
|
|
|
|
for (const state of this.layers) {
|
2021-07-15 12:00:19 -07:00
|
|
|
this.map!.removeLayer(state.layer);
|
2021-07-09 08:53:07 -07:00
|
|
|
state.layer.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!layers) {
|
|
|
|
|
layers = [];
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
const legends: React.ReactNode[] = [];
|
2021-07-09 08:53:07 -07:00
|
|
|
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?
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-21 23:41:27 -07:00
|
|
|
const handler = await item.create(this.map!, overlay, config.theme2);
|
2021-07-09 08:53:07 -07:00
|
|
|
const layer = handler.init();
|
2021-07-28 18:34:42 -07:00
|
|
|
(layer as any).___handler = handler;
|
2021-07-15 12:00:19 -07:00
|
|
|
this.map!.addLayer(layer);
|
2021-07-09 08:53:07 -07:00
|
|
|
this.layers.push({
|
|
|
|
|
config: overlay,
|
|
|
|
|
layer,
|
|
|
|
|
handler,
|
|
|
|
|
});
|
2021-07-26 18:50:15 -04:00
|
|
|
|
|
|
|
|
if (handler.legend) {
|
|
|
|
|
legends.push(<div key={`${this.counter++}`}>{handler.legend}</div>);
|
|
|
|
|
}
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
2021-07-26 18:50:15 -04:00
|
|
|
this.setState({ bottomLeft: legends });
|
2021-07-21 23:41:27 -07:00
|
|
|
|
|
|
|
|
// Update data after init layers
|
|
|
|
|
this.dataChanged(this.props.data);
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initMapView(config: MapViewConfig): View {
|
|
|
|
|
let view = new View({
|
|
|
|
|
center: [0, 0],
|
|
|
|
|
zoom: 1,
|
2021-07-23 16:23:35 -07:00
|
|
|
showFullExtent: true, // alows zooming so the full range is visiable
|
2021-07-09 08:53:07 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// With shared views, all panels use the same view instance
|
|
|
|
|
if (config.shared) {
|
|
|
|
|
if (!sharedView) {
|
|
|
|
|
sharedView = view;
|
|
|
|
|
} else {
|
|
|
|
|
view = sharedView;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-19 08:40:56 -07:00
|
|
|
const v = centerPointRegistry.getIfExists(config.id);
|
2021-07-09 08:53:07 -07:00
|
|
|
if (v) {
|
|
|
|
|
let coord: Coordinate | undefined = undefined;
|
|
|
|
|
if (v.lat == null) {
|
|
|
|
|
if (v.id === MapCenterID.Coordinates) {
|
2021-07-19 08:40:56 -07:00
|
|
|
coord = [config.lon ?? 0, config.lat ?? 0];
|
2021-07-09 08:53:07 -07:00
|
|
|
} 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) {
|
2021-07-15 12:00:19 -07:00
|
|
|
if (!this.map) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-07-09 08:53:07 -07:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 12:00:19 -07:00
|
|
|
this.mouseWheelZoom!.setActive(Boolean(options.mouseWheelZoom));
|
2021-07-09 08:53:07 -07:00
|
|
|
|
|
|
|
|
if (options.showAttribution) {
|
|
|
|
|
this.map.addControl(new Attribution({ collapsed: true, collapsible: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the react overlays
|
2021-07-26 18:50:15 -04:00
|
|
|
let topRight: ReactNode[] = [];
|
2021-07-09 08:53:07 -07:00
|
|
|
if (options.showDebug) {
|
2021-07-26 18:50:15 -04:00
|
|
|
topRight = [<DebugOverlay key="debug" map={this.map} />];
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
2021-07-26 18:50:15 -04:00
|
|
|
this.setState({ topRight });
|
2021-07-09 08:53:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2021-07-28 18:34:42 -07:00
|
|
|
const { ttip, topRight, bottomLeft } = this.state;
|
|
|
|
|
|
2021-07-09 08:53:07 -07:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Global styles={this.globalCSS} />
|
|
|
|
|
<div className={this.style.wrap}>
|
|
|
|
|
<div className={this.style.map} ref={this.initMapRef}></div>
|
2021-07-28 18:34:42 -07:00
|
|
|
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
|
2021-07-09 08:53:07 -07:00
|
|
|
</div>
|
2021-07-28 18:34:42 -07:00
|
|
|
<Portal>
|
2021-09-23 19:31:36 -07:00
|
|
|
{ttip && (ttip.data || ttip.feature) && (
|
2021-07-28 18:34:42 -07:00
|
|
|
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }}>
|
|
|
|
|
<DataHoverView {...ttip} />
|
|
|
|
|
</VizTooltipContainer>
|
|
|
|
|
)}
|
|
|
|
|
</Portal>
|
2021-07-09 08:53:07 -07:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
|
|
|
|
wrap: css`
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
`,
|
|
|
|
|
map: css`
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
`,
|
|
|
|
|
}));
|