mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Cleanup GeomapPanel component (#54538)
This commit is contained in:
parent
cdc2200273
commit
3864de9425
@ -8088,33 +8088,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/GeomapPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/MarkersLegend.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@ -8169,15 +8148,20 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/module.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/style/utils.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/utils/layers.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/utils/selection.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/utils/tootltip.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/view.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
@ -1,32 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Global } from '@emotion/react';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Collection, Map as OpenLayersMap, MapBrowserEvent, PluggableMap, View } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { Map as OpenLayersMap, MapBrowserEvent, View } from 'ol';
|
||||
import Attribution from 'ol/control/Attribution';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataHoverClearEvent,
|
||||
DataHoverEvent,
|
||||
FrameGeometrySourceMode,
|
||||
getFrameMatchers,
|
||||
GrafanaTheme,
|
||||
MapLayerHandler,
|
||||
MapLayerOptions,
|
||||
PanelData,
|
||||
PanelProps,
|
||||
} from '@grafana/data';
|
||||
import { DataHoverEvent, GrafanaTheme, PanelData, PanelProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
|
||||
import { PanelEditExitedEvent } from 'app/types/events';
|
||||
@ -36,13 +18,16 @@ import { GeomapTooltip } from './GeomapTooltip';
|
||||
import { DebugOverlay } from './components/DebugOverlay';
|
||||
import { MeasureOverlay } from './components/MeasureOverlay';
|
||||
import { MeasureVectorLayer } from './components/MeasureVectorLayer';
|
||||
import { GeomapHoverPayload, GeomapLayerHover } from './event';
|
||||
import { GeomapHoverPayload } from './event';
|
||||
import { getGlobalStyles } from './globalStyles';
|
||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
|
||||
import { getLayersExtent } from './utils/getLayersExtent';
|
||||
import { centerPointRegistry, MapCenterID } from './view';
|
||||
import { defaultMarkersConfig } from './layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG } from './layers/registry';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, TooltipMode } from './types';
|
||||
import { getActions } from './utils/actions';
|
||||
import { applyLayerFilter, initLayer } from './utils/layers';
|
||||
import { pointerClickListener, pointerMoveListener, setTooltipListeners } from './utils/tootltip';
|
||||
import { updateMap, getNewOpenLayersMap, notifyPanelEditor } from './utils/utils';
|
||||
import { initMapView, initViewExtent } from './utils/view';
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
let sharedView: View | undefined = undefined;
|
||||
@ -55,24 +40,9 @@ interface State extends OverlayProps {
|
||||
measureMenuActive?: boolean;
|
||||
}
|
||||
|
||||
export interface GeomapLayerActions {
|
||||
selectLayer: (uid: string) => void;
|
||||
deleteLayer: (uid: string) => void;
|
||||
addlayer: (type: string) => void;
|
||||
reorder: (src: number, dst: number) => void;
|
||||
canRename: (v: string) => boolean;
|
||||
}
|
||||
|
||||
export interface GeomapInstanceState {
|
||||
map?: OpenLayersMap;
|
||||
layers: MapLayerState[];
|
||||
selected: number;
|
||||
actions: GeomapLayerActions;
|
||||
}
|
||||
|
||||
export class GeomapPanel extends Component<Props, State> {
|
||||
static contextType = PanelContextRoot;
|
||||
panelContext: PanelContext = {} as PanelContext;
|
||||
panelContext: PanelContext | undefined = undefined;
|
||||
private subs = new Subscription();
|
||||
|
||||
globalCSS = getGlobalStyles(config.theme2);
|
||||
@ -100,7 +70,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.panelContext = this.context as PanelContext;
|
||||
this.panelContext = this.context;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -144,7 +114,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
/** This function will actually update the JSON model */
|
||||
private doOptionsUpdate(selected: number) {
|
||||
doOptionsUpdate(selected: number) {
|
||||
const { options, onOptionsChange } = this.props;
|
||||
const layers = this.layers;
|
||||
this.map?.getLayers().forEach((l) => {
|
||||
@ -159,95 +129,11 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
layers: layers.slice(1).map((v) => v.options),
|
||||
});
|
||||
|
||||
// Notify the panel editor
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: layers,
|
||||
selected,
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
|
||||
notifyPanelEditor(this, layers, selected);
|
||||
this.setState({ legends: this.getLegends() });
|
||||
}
|
||||
|
||||
getNextLayerName = () => {
|
||||
let idx = this.layers.length; // since basemap is 0, this looks right
|
||||
while (true && idx < 100) {
|
||||
const name = `Layer ${idx++}`;
|
||||
if (!this.byName.has(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return `Layer ${Date.now()}`;
|
||||
};
|
||||
|
||||
actions: GeomapLayerActions = {
|
||||
selectLayer: (uid: string) => {
|
||||
const selected = this.layers.findIndex((v) => v.options.name === uid);
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: this.layers,
|
||||
selected,
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
},
|
||||
canRename: (v: string) => {
|
||||
return !this.byName.has(v);
|
||||
},
|
||||
deleteLayer: (uid: string) => {
|
||||
const layers: MapLayerState[] = [];
|
||||
for (const lyr of this.layers) {
|
||||
if (lyr.options.name === uid) {
|
||||
this.map?.removeLayer(lyr.layer);
|
||||
} else {
|
||||
layers.push(lyr);
|
||||
}
|
||||
}
|
||||
this.layers = layers;
|
||||
this.doOptionsUpdate(0);
|
||||
},
|
||||
addlayer: (type: string) => {
|
||||
const item = geomapLayerRegistry.getIfExists(type);
|
||||
if (!item) {
|
||||
return; // ignore empty request
|
||||
}
|
||||
this.initLayer(
|
||||
this.map!,
|
||||
{
|
||||
type: item.id,
|
||||
name: this.getNextLayerName(),
|
||||
config: cloneDeep(item.defaultOptions),
|
||||
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
|
||||
tooltip: true,
|
||||
},
|
||||
false
|
||||
).then((lyr) => {
|
||||
this.layers = this.layers.slice(0);
|
||||
this.layers.push(lyr);
|
||||
this.map?.addLayer(lyr.layer);
|
||||
|
||||
this.doOptionsUpdate(this.layers.length - 1);
|
||||
});
|
||||
},
|
||||
reorder: (startIndex: number, endIndex: number) => {
|
||||
// TODO look into reorder with respect to measure layer
|
||||
const result = Array.from(this.layers);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
this.layers = result;
|
||||
|
||||
this.doOptionsUpdate(endIndex);
|
||||
|
||||
// Add the layers in the right order
|
||||
const group = this.map?.getLayers()!;
|
||||
group.clear();
|
||||
this.layers.forEach((v) => group.push(v.layer));
|
||||
},
|
||||
};
|
||||
actions = getActions(this);
|
||||
|
||||
/**
|
||||
* Called when the panel options change
|
||||
@ -257,7 +143,10 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
optionsChanged(options: GeomapPanelOptions) {
|
||||
const oldOptions = this.props.options;
|
||||
if (options.view !== oldOptions.view) {
|
||||
this.map!.setView(this.initMapView(options.view, this.map!.getLayers()));
|
||||
const [updatedSharedView, view] = initMapView(options.view, sharedView, this.map!.getLayers());
|
||||
sharedView = updatedSharedView;
|
||||
// eslint-disable-next-line
|
||||
this.map!.setView(view as View);
|
||||
}
|
||||
|
||||
if (options.controls !== oldOptions.controls) {
|
||||
@ -272,7 +161,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
// Only update if panel data matches component data
|
||||
if (data === this.props.data) {
|
||||
for (const state of this.layers) {
|
||||
this.applyLayerFilter(state.handler, state.options);
|
||||
applyLayerFilter(state.handler, state.options, this.props.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -284,35 +173,27 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
if (!div) {
|
||||
// eslint-disable-next-line
|
||||
this.map = undefined as unknown as OpenLayersMap;
|
||||
return;
|
||||
}
|
||||
const { options } = this.props;
|
||||
|
||||
const map = (this.map = new OpenLayersMap({
|
||||
view: this.initMapView(options.view, undefined),
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
controls: [],
|
||||
target: div,
|
||||
interactions: interactionDefaults({
|
||||
mouseWheelZoom: false, // managed by initControls
|
||||
}),
|
||||
}));
|
||||
const map = getNewOpenLayersMap(this, options, div);
|
||||
|
||||
this.byName.clear();
|
||||
const layers: MapLayerState[] = [];
|
||||
try {
|
||||
layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||
layers.push(await initLayer(this, map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||
|
||||
// Default layer values
|
||||
const layerOptions = options.layers ?? [defaultMarkersConfig];
|
||||
|
||||
for (const lyr of layerOptions) {
|
||||
layers.push(await this.initLayer(map, lyr, false));
|
||||
layers.push(await initLayer(this, map, lyr, false));
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('error loading layers', ex);
|
||||
console.error('error loading layers', ex); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
for (const lyr of layers) {
|
||||
@ -320,29 +201,14 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
this.layers = layers;
|
||||
this.map = map; // redundant
|
||||
this.initViewExtent(map.getView(), options.view, map.getLayers());
|
||||
initViewExtent(map.getView(), options.view, map.getLayers());
|
||||
|
||||
this.mouseWheelZoom = new MouseWheelZoom();
|
||||
this.map.addInteraction(this.mouseWheelZoom);
|
||||
this.initControls(options.controls);
|
||||
this.forceUpdate(); // first render
|
||||
this.map?.addInteraction(this.mouseWheelZoom);
|
||||
|
||||
// Tooltip listener
|
||||
this.map.on('singleclick', this.pointerClickListener);
|
||||
this.map.on('pointermove', this.pointerMoveListener);
|
||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||
});
|
||||
|
||||
// Notify the panel editor
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: layers,
|
||||
selected: layers.length - 1, // the top layer
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
updateMap(this, options);
|
||||
setTooltipListeners(this);
|
||||
notifyPanelEditor(this, layers, layers.length - 1);
|
||||
|
||||
this.setState({ legends: this.getLegends() });
|
||||
};
|
||||
@ -358,283 +224,13 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
};
|
||||
|
||||
pointerClickListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
if (this.pointerMoveListener(evt)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.mapDiv!.style.cursor = 'auto';
|
||||
this.setState({ ttipOpen: true });
|
||||
}
|
||||
pointerClickListener(evt, this);
|
||||
};
|
||||
|
||||
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
// If measure menu is open, bypass tooltip logic and display measuring mouse events
|
||||
if (this.state.measureMenuActive) {
|
||||
return true;
|
||||
}
|
||||
if (!this.map || this.state.ttipOpen) {
|
||||
return false;
|
||||
}
|
||||
const mouse = evt.originalEvent as MouseEvent;
|
||||
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;
|
||||
hoverPayload.layers = undefined;
|
||||
|
||||
const layers: GeomapLayerHover[] = [];
|
||||
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
|
||||
|
||||
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
||||
this.map.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer, geo) => {
|
||||
const s: MapLayerState = (layer as any).__state;
|
||||
//match hover layer to layer in layers
|
||||
//check if the layer show tooltip is enabled
|
||||
//then also pass the list of tooltip fields if exists
|
||||
//this is used as the generic hover event
|
||||
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'];
|
||||
}
|
||||
|
||||
if (s?.mouseEvents) {
|
||||
s.mouseEvents.next(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
let h = layerLookup.get(s);
|
||||
if (!h) {
|
||||
h = { layer: s, features: [] };
|
||||
layerLookup.set(s, h);
|
||||
layers.push(h);
|
||||
}
|
||||
h.features.push(feature);
|
||||
}
|
||||
},
|
||||
{
|
||||
layerFilter: (l) => {
|
||||
const hoverLayerState = (l as any).__state as MapLayerState;
|
||||
return hoverLayerState?.options?.tooltip !== false;
|
||||
},
|
||||
}
|
||||
);
|
||||
this.hoverPayload.layers = layers.length ? layers : undefined;
|
||||
this.props.eventBus.publish(this.hoverEvent);
|
||||
|
||||
this.setState({ ttip: { ...hoverPayload } });
|
||||
|
||||
if (!layers.length) {
|
||||
// clear mouse events
|
||||
this.layers.forEach((layer) => {
|
||||
layer.mouseEvents.next(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
const found = Boolean(layers.length);
|
||||
this.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
|
||||
return found;
|
||||
pointerMoveListener(evt, this);
|
||||
};
|
||||
|
||||
private updateLayer = async (uid: string, newOptions: MapLayerOptions): Promise<boolean> => {
|
||||
if (!this.map) {
|
||||
return false;
|
||||
}
|
||||
const current = this.byName.get(uid);
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let layerIndex = -1;
|
||||
const group = this.map?.getLayers()!;
|
||||
for (let i = 0; i < group?.getLength(); i++) {
|
||||
if (group.item(i) === current.layer) {
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for rename
|
||||
if (newOptions.name !== uid) {
|
||||
if (!newOptions.name) {
|
||||
newOptions.name = uid;
|
||||
} else if (this.byName.has(newOptions.name)) {
|
||||
return false;
|
||||
}
|
||||
this.byName.delete(uid);
|
||||
|
||||
uid = newOptions.name;
|
||||
this.byName.set(uid, current);
|
||||
}
|
||||
|
||||
// Type changed -- requires full re-initalization
|
||||
if (current.options.type !== newOptions.type) {
|
||||
// full init
|
||||
} else {
|
||||
// just update options
|
||||
}
|
||||
|
||||
const layers = this.layers.slice(0);
|
||||
try {
|
||||
const info = await this.initLayer(this.map, newOptions, current.isBasemap);
|
||||
layers[layerIndex]?.handler.dispose?.();
|
||||
layers[layerIndex] = info;
|
||||
group.setAt(layerIndex, info.layer);
|
||||
|
||||
// initialize with new data
|
||||
this.applyLayerFilter(info.handler, newOptions);
|
||||
} catch (err) {
|
||||
console.warn('ERROR', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just to trigger a state update
|
||||
this.setState({ legends: [] });
|
||||
|
||||
this.layers = layers;
|
||||
this.doOptionsUpdate(layerIndex);
|
||||
return true;
|
||||
};
|
||||
|
||||
async initLayer(map: PluggableMap, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
|
||||
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||
options = DEFAULT_BASEMAP_CONFIG;
|
||||
}
|
||||
|
||||
// Use default makers layer
|
||||
if (!options?.type) {
|
||||
options = {
|
||||
type: MARKERS_LAYER_ID,
|
||||
name: this.getNextLayerName(),
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
const item = geomapLayerRegistry.getIfExists(options.type);
|
||||
if (!item) {
|
||||
return Promise.reject('unknown layer: ' + options.type);
|
||||
}
|
||||
|
||||
const handler = await item.create(map, options, this.props.eventBus, config.theme2);
|
||||
const layer = handler.init();
|
||||
if (options.opacity != null) {
|
||||
layer.setOpacity(options.opacity);
|
||||
}
|
||||
|
||||
if (!options.name) {
|
||||
options.name = this.getNextLayerName();
|
||||
}
|
||||
|
||||
const UID = options.name;
|
||||
const state: MapLayerState<unknown> = {
|
||||
// UID, // unique name when added to the map (it may change and will need special handling)
|
||||
isBasemap,
|
||||
options,
|
||||
layer,
|
||||
handler,
|
||||
mouseEvents: new Subject<FeatureLike | undefined>(),
|
||||
|
||||
getName: () => UID,
|
||||
|
||||
// Used by the editors
|
||||
onChange: (cfg: MapLayerOptions) => {
|
||||
this.updateLayer(UID, cfg);
|
||||
},
|
||||
};
|
||||
|
||||
this.byName.set(UID, state);
|
||||
(state.layer as any).__state = state;
|
||||
|
||||
this.applyLayerFilter(handler, options);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
applyLayerFilter(handler: MapLayerHandler<unknown>, options: MapLayerOptions<unknown>): void {
|
||||
if (handler.update) {
|
||||
let panelData = this.props.data;
|
||||
if (options.filterData) {
|
||||
const matcherFunc = getFrameMatchers(options.filterData);
|
||||
panelData = {
|
||||
...panelData,
|
||||
series: panelData.series.filter(matcherFunc),
|
||||
};
|
||||
}
|
||||
handler.update(panelData);
|
||||
}
|
||||
}
|
||||
|
||||
initMapView(config: MapViewConfig, layers?: Collection<BaseLayer>): View {
|
||||
let view = new View({
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
showFullExtent: true, // allows zooming so the full range is visible
|
||||
});
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
if (!sharedView) {
|
||||
sharedView = view;
|
||||
} else {
|
||||
view = sharedView;
|
||||
}
|
||||
}
|
||||
if (layers) {
|
||||
this.initViewExtent(view, config, layers);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
initViewExtent(view: View, config: MapViewConfig, layers: Collection<BaseLayer>) {
|
||||
const v = centerPointRegistry.getIfExists(config.id);
|
||||
if (v) {
|
||||
let coord: Coordinate | undefined = undefined;
|
||||
if (v.lat == null) {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||
} else if (v.id === MapCenterID.Fit) {
|
||||
const extent = getLayersExtent(layers);
|
||||
if (!isEmpty(extent)) {
|
||||
view.fit(extent, {
|
||||
padding: [30, 30, 30, 30],
|
||||
maxZoom: config.zoom ?? config.maxZoom,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO: view requires special handling
|
||||
}
|
||||
} 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 && v?.id !== MapCenterID.Fit) {
|
||||
view.setZoom(config.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
initControls(options: ControlsOptions) {
|
||||
if (!this.map) {
|
||||
return;
|
||||
|
@ -63,7 +63,7 @@ export const generateLabel = (feature: FeatureLike, idx: number): string => {
|
||||
const names = ['Name', 'name', 'Title', 'ID', 'id'];
|
||||
let props = feature.getProperties();
|
||||
let first = '';
|
||||
const frame = feature.get('frame') as DataFrame;
|
||||
const frame = feature.get('frame') as DataFrame; // eslint-disable-line
|
||||
if (frame) {
|
||||
const rowIndex = feature.get('rowIndex');
|
||||
for (const f of frame.fields) {
|
||||
|
@ -36,13 +36,13 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
||||
}
|
||||
|
||||
const props = hoverEvent.getProperties();
|
||||
const frame = props.frame as DataFrame;
|
||||
const frame = props.frame as DataFrame; // eslint-disable-line
|
||||
|
||||
if (!frame) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rowIndex = props.rowIndex as number;
|
||||
const rowIndex = props.rowIndex as number; // eslint-disable-line
|
||||
return colorField.values.get(rowIndex);
|
||||
}, [hoverEvent, colorField]);
|
||||
|
||||
|
@ -6,9 +6,8 @@ import { Container } from '@grafana/ui';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
|
||||
|
||||
import { GeomapInstanceState } from '../GeomapPanel';
|
||||
import { getLayersOptions } from '../layers/registry';
|
||||
import { GeomapPanelOptions, MapLayerState } from '../types';
|
||||
import { GeomapPanelOptions, MapLayerState, GeomapInstanceState } from '../types';
|
||||
|
||||
type LayersEditorProps = StandardEditorProps<unknown, unknown, GeomapPanelOptions, GeomapInstanceState>;
|
||||
|
||||
|
@ -5,8 +5,7 @@ import { StandardEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
|
||||
import { GeomapInstanceState } from '../GeomapPanel';
|
||||
import { GeomapPanelOptions, MapViewConfig } from '../types';
|
||||
import { GeomapPanelOptions, MapViewConfig, GeomapInstanceState } from '../types';
|
||||
import { centerPointRegistry, MapCenterID } from '../view';
|
||||
|
||||
export const MapViewEditor: FC<
|
||||
|
@ -4,12 +4,12 @@ import { PanelPlugin } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { commonOptionsBuilder } from '@grafana/ui';
|
||||
|
||||
import { GeomapInstanceState, GeomapPanel } from './GeomapPanel';
|
||||
import { GeomapPanel } from './GeomapPanel';
|
||||
import { LayersEditor } from './editor/LayersEditor';
|
||||
import { MapViewEditor } from './editor/MapViewEditor';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||
import { defaultView, GeomapPanelOptions, TooltipMode } from './types';
|
||||
import { defaultView, GeomapPanelOptions, TooltipMode, GeomapInstanceState } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
.setNoPadding()
|
||||
@ -40,6 +40,7 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
defaultValue: defaultView.shared,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const state = context.instanceState as GeomapInstanceState;
|
||||
if (!state?.layers) {
|
||||
// TODO? show spinner?
|
||||
|
@ -267,7 +267,7 @@ async function prepareSVG(url: string, size?: number): Promise<string> {
|
||||
return `data:image/svg+xml,${svgURI}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Map as OpenLayersMap } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import Units from 'ol/proj/Units';
|
||||
@ -69,16 +70,33 @@ export interface GeomapPanelOptions {
|
||||
layers: MapLayerOptions[];
|
||||
tooltip: TooltipOptions;
|
||||
}
|
||||
|
||||
export interface FeatureStyleConfig {
|
||||
style?: StyleConfig;
|
||||
check?: FeatureRuleConfig;
|
||||
}
|
||||
|
||||
export interface FeatureRuleConfig {
|
||||
property: string;
|
||||
operation: ComparisonOperation;
|
||||
value: string | boolean | number;
|
||||
}
|
||||
|
||||
export interface GeomapLayerActions {
|
||||
selectLayer: (uid: string) => void;
|
||||
deleteLayer: (uid: string) => void;
|
||||
addlayer: (type: string) => void;
|
||||
reorder: (src: number, dst: number) => void;
|
||||
canRename: (v: string) => boolean;
|
||||
}
|
||||
|
||||
export interface GeomapInstanceState {
|
||||
map?: OpenLayersMap;
|
||||
layers: MapLayerState[];
|
||||
selected: number;
|
||||
actions: GeomapLayerActions;
|
||||
}
|
||||
|
||||
export enum ComparisonOperation {
|
||||
EQ = 'eq',
|
||||
NEQ = 'neq',
|
||||
|
80
public/app/plugins/panel/geomap/utils/actions.ts
Normal file
80
public/app/plugins/panel/geomap/utils/actions.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { FrameGeometrySourceMode } from '@grafana/data/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { geomapLayerRegistry } from '../layers/registry';
|
||||
import { GeomapLayerActions, MapLayerState } from '../types';
|
||||
|
||||
import { initLayer } from './layers';
|
||||
import { getNextLayerName } from './utils';
|
||||
|
||||
export const getActions = (panel: GeomapPanel) => {
|
||||
const actions: GeomapLayerActions = {
|
||||
selectLayer: (uid: string) => {
|
||||
const selected = panel.layers.findIndex((v) => v.options.name === uid);
|
||||
if (panel.panelContext && panel.panelContext.onInstanceStateChange) {
|
||||
panel.panelContext.onInstanceStateChange({
|
||||
map: panel.map,
|
||||
layers: panel.layers,
|
||||
selected,
|
||||
actions: panel.actions,
|
||||
});
|
||||
}
|
||||
},
|
||||
canRename: (v: string) => {
|
||||
return !panel.byName.has(v);
|
||||
},
|
||||
deleteLayer: (uid: string) => {
|
||||
const layers: MapLayerState[] = [];
|
||||
for (const lyr of panel.layers) {
|
||||
if (lyr.options.name === uid) {
|
||||
panel.map?.removeLayer(lyr.layer);
|
||||
} else {
|
||||
layers.push(lyr);
|
||||
}
|
||||
}
|
||||
panel.layers = layers;
|
||||
panel.doOptionsUpdate(0);
|
||||
},
|
||||
addlayer: (type: string) => {
|
||||
const item = geomapLayerRegistry.getIfExists(type);
|
||||
if (!item) {
|
||||
return; // ignore empty request
|
||||
}
|
||||
initLayer(
|
||||
panel,
|
||||
panel.map!,
|
||||
{
|
||||
type: item.id,
|
||||
name: getNextLayerName(panel),
|
||||
config: cloneDeep(item.defaultOptions),
|
||||
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
|
||||
tooltip: true,
|
||||
},
|
||||
false
|
||||
).then((lyr) => {
|
||||
panel.layers = panel.layers.slice(0);
|
||||
panel.layers.push(lyr);
|
||||
panel.map?.addLayer(lyr.layer);
|
||||
|
||||
panel.doOptionsUpdate(panel.layers.length - 1);
|
||||
});
|
||||
},
|
||||
reorder: (startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(panel.layers);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
panel.layers = result;
|
||||
|
||||
panel.doOptionsUpdate(endIndex);
|
||||
|
||||
// Add the layers in the right order
|
||||
const group = panel.map?.getLayers()!;
|
||||
group.clear();
|
||||
panel.layers.forEach((v) => group.push(v.layer));
|
||||
},
|
||||
};
|
||||
|
||||
return actions;
|
||||
};
|
155
public/app/plugins/panel/geomap/utils/layers.ts
Normal file
155
public/app/plugins/panel/geomap/utils/layers.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { PluggableMap } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { getFrameMatchers, MapLayerHandler, MapLayerOptions, PanelData } from '@grafana/data/src';
|
||||
import { config } from '@grafana/runtime/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { MARKERS_LAYER_ID } from '../layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
|
||||
import { MapLayerState } from '../types';
|
||||
|
||||
import { getNextLayerName } from './utils';
|
||||
|
||||
export const applyLayerFilter = (
|
||||
handler: MapLayerHandler<unknown>,
|
||||
options: MapLayerOptions<unknown>,
|
||||
panelDataProps: PanelData
|
||||
): void => {
|
||||
if (handler.update) {
|
||||
let panelData = panelDataProps;
|
||||
if (options.filterData) {
|
||||
const matcherFunc = getFrameMatchers(options.filterData);
|
||||
panelData = {
|
||||
...panelData,
|
||||
series: panelData.series.filter(matcherFunc),
|
||||
};
|
||||
}
|
||||
handler.update(panelData);
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateLayer(panel: GeomapPanel, uid: string, newOptions: MapLayerOptions): Promise<boolean> {
|
||||
if (!panel.map) {
|
||||
return false;
|
||||
}
|
||||
const current = panel.byName.get(uid);
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let layerIndex = -1;
|
||||
const group = panel.map?.getLayers()!;
|
||||
for (let i = 0; i < group?.getLength(); i++) {
|
||||
if (group.item(i) === current.layer) {
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for rename
|
||||
if (newOptions.name !== uid) {
|
||||
if (!newOptions.name) {
|
||||
newOptions.name = uid;
|
||||
} else if (panel.byName.has(newOptions.name)) {
|
||||
return false;
|
||||
}
|
||||
panel.byName.delete(uid);
|
||||
|
||||
uid = newOptions.name;
|
||||
panel.byName.set(uid, current);
|
||||
}
|
||||
|
||||
// Type changed -- requires full re-initalization
|
||||
if (current.options.type !== newOptions.type) {
|
||||
// full init
|
||||
} else {
|
||||
// just update options
|
||||
}
|
||||
|
||||
const layers = panel.layers.slice(0);
|
||||
try {
|
||||
const info = await initLayer(panel, panel.map, newOptions, current.isBasemap);
|
||||
layers[layerIndex]?.handler.dispose?.();
|
||||
layers[layerIndex] = info;
|
||||
group.setAt(layerIndex, info.layer);
|
||||
|
||||
// initialize with new data
|
||||
applyLayerFilter(info.handler, newOptions, panel.props.data);
|
||||
} catch (err) {
|
||||
console.warn('ERROR', err); // eslint-disable-line no-console
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just to trigger a state update
|
||||
panel.setState({ legends: [] });
|
||||
|
||||
panel.layers = layers;
|
||||
panel.doOptionsUpdate(layerIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function initLayer(
|
||||
panel: GeomapPanel,
|
||||
map: PluggableMap,
|
||||
options: MapLayerOptions,
|
||||
isBasemap?: boolean
|
||||
): Promise<MapLayerState> {
|
||||
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||
options = DEFAULT_BASEMAP_CONFIG;
|
||||
}
|
||||
|
||||
// Use default makers layer
|
||||
if (!options?.type) {
|
||||
options = {
|
||||
type: MARKERS_LAYER_ID,
|
||||
name: getNextLayerName(panel),
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
const item = geomapLayerRegistry.getIfExists(options.type);
|
||||
if (!item) {
|
||||
return Promise.reject('unknown layer: ' + options.type);
|
||||
}
|
||||
|
||||
const handler = await item.create(map, options, panel.props.eventBus, config.theme2);
|
||||
const layer = handler.init(); // eslint-disable-line
|
||||
if (options.opacity != null) {
|
||||
layer.setOpacity(options.opacity);
|
||||
}
|
||||
|
||||
if (!options.name) {
|
||||
options.name = getNextLayerName(panel);
|
||||
}
|
||||
|
||||
const UID = options.name;
|
||||
const state: MapLayerState<unknown> = {
|
||||
// UID, // unique name when added to the map (it may change and will need special handling)
|
||||
isBasemap,
|
||||
options,
|
||||
layer,
|
||||
handler,
|
||||
mouseEvents: new Subject<FeatureLike | undefined>(),
|
||||
|
||||
getName: () => UID,
|
||||
|
||||
// Used by the editors
|
||||
onChange: (cfg: MapLayerOptions) => {
|
||||
updateLayer(panel, UID, cfg);
|
||||
},
|
||||
};
|
||||
|
||||
panel.byName.set(UID, state);
|
||||
// eslint-disable-next-line
|
||||
(state.layer as any).__state = state;
|
||||
|
||||
applyLayerFilter(handler, options, panel.props.data);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const getMapLayerState = (l: any) => {
|
||||
return l?.__state as MapLayerState;
|
||||
};
|
111
public/app/plugins/panel/geomap/utils/tootltip.ts
Normal file
111
public/app/plugins/panel/geomap/utils/tootltip.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { MapBrowserEvent } from 'ol';
|
||||
import { toLonLat } from 'ol/proj';
|
||||
|
||||
import { DataFrame, DataHoverClearEvent } from '@grafana/data/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { GeomapHoverPayload, GeomapLayerHover } from '../event';
|
||||
import { MapLayerState } from '../types';
|
||||
|
||||
import { getMapLayerState } from './layers';
|
||||
|
||||
export const setTooltipListeners = (panel: GeomapPanel) => {
|
||||
// Tooltip listener
|
||||
panel.map?.on('singleclick', panel.pointerClickListener);
|
||||
panel.map?.on('pointermove', panel.pointerMoveListener);
|
||||
panel.map?.getViewport().addEventListener('mouseout', (evt: MouseEvent) => {
|
||||
panel.props.eventBus.publish(new DataHoverClearEvent());
|
||||
});
|
||||
};
|
||||
|
||||
export const pointerClickListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
|
||||
if (pointerMoveListener(evt, panel)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
panel.mapDiv!.style.cursor = 'auto';
|
||||
panel.setState({ ttipOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
export const pointerMoveListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
|
||||
// If measure menu is open, bypass tooltip logic and display measuring mouse events
|
||||
if (panel.state.measureMenuActive) {
|
||||
return true;
|
||||
}
|
||||
if (!panel.map || panel.state.ttipOpen) {
|
||||
return false;
|
||||
}
|
||||
const mouse = evt.originalEvent as MouseEvent; // eslint-disable-line
|
||||
const pixel = panel.map.getEventPixel(mouse);
|
||||
const hover = toLonLat(panel.map.getCoordinateFromPixel(pixel));
|
||||
|
||||
const { hoverPayload } = panel;
|
||||
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;
|
||||
hoverPayload.layers = undefined;
|
||||
|
||||
const layers: GeomapLayerHover[] = [];
|
||||
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
|
||||
|
||||
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
||||
panel.map.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer, geo) => {
|
||||
const s: MapLayerState = getMapLayerState(layer);
|
||||
//match hover layer to layer in layers
|
||||
//check if the layer show tooltip is enabled
|
||||
//then also pass the list of tooltip fields if exists
|
||||
//this is used as the generic hover event
|
||||
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'];
|
||||
}
|
||||
|
||||
if (s?.mouseEvents) {
|
||||
s.mouseEvents.next(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
let h = layerLookup.get(s);
|
||||
if (!h) {
|
||||
h = { layer: s, features: [] };
|
||||
layerLookup.set(s, h);
|
||||
layers.push(h);
|
||||
}
|
||||
h.features.push(feature);
|
||||
}
|
||||
},
|
||||
{
|
||||
layerFilter: (l) => {
|
||||
const hoverLayerState = getMapLayerState(l);
|
||||
return hoverLayerState?.options?.tooltip !== false;
|
||||
},
|
||||
}
|
||||
);
|
||||
panel.hoverPayload.layers = layers.length ? layers : undefined;
|
||||
panel.props.eventBus.publish(panel.hoverEvent);
|
||||
|
||||
panel.setState({ ttip: { ...hoverPayload } });
|
||||
|
||||
if (!layers.length) {
|
||||
// clear mouse events
|
||||
panel.layers.forEach((layer) => {
|
||||
layer.mouseEvents.next(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
const found = Boolean(layers.length);
|
||||
panel.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
|
||||
return found;
|
||||
};
|
@ -1,9 +1,16 @@
|
||||
import { Map as OpenLayersMap } from 'ol';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { getColorDimension, getScalarDimension, getScaledDimension, getTextDimension } from 'app/features/dimensions';
|
||||
import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { defaultStyleConfig, StyleConfig, StyleConfigState, StyleDimensions } from '../style/types';
|
||||
import { GeomapPanelOptions, MapLayerState } from '../types';
|
||||
|
||||
import { initMapView } from './view';
|
||||
|
||||
export function getStyleDimension(
|
||||
frame: DataFrame | undefined,
|
||||
@ -68,3 +75,46 @@ async function initGeojsonFiles() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const getNewOpenLayersMap = (panel: GeomapPanel, options: GeomapPanelOptions, div: HTMLDivElement) => {
|
||||
const [view] = initMapView(options.view, undefined, undefined);
|
||||
return (panel.map = new OpenLayersMap({
|
||||
view: view,
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
controls: [],
|
||||
target: div,
|
||||
interactions: interactionDefaults({
|
||||
mouseWheelZoom: false, // managed by initControls
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateMap = (panel: GeomapPanel, options: GeomapPanelOptions) => {
|
||||
panel.initControls(options.controls);
|
||||
panel.forceUpdate(); // first render
|
||||
};
|
||||
|
||||
export const notifyPanelEditor = (geomapPanel: GeomapPanel, layers: MapLayerState[], selected: number) => {
|
||||
// Notify the panel editor
|
||||
if (geomapPanel.panelContext && geomapPanel.panelContext.onInstanceStateChange) {
|
||||
geomapPanel.panelContext.onInstanceStateChange({
|
||||
map: geomapPanel.map,
|
||||
layers: layers,
|
||||
selected: selected,
|
||||
actions: geomapPanel.actions,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getNextLayerName = (panel: GeomapPanel) => {
|
||||
let idx = panel.layers.length; // since basemap is 0, this looks right
|
||||
while (true && idx < 100) {
|
||||
const name = `Layer ${idx++}`;
|
||||
if (!panel.byName.has(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return `Layer ${Date.now()}`;
|
||||
};
|
||||
|
73
public/app/plugins/panel/geomap/utils/view.ts
Normal file
73
public/app/plugins/panel/geomap/utils/view.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { View, Collection } from 'ol';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
|
||||
import { MapViewConfig } from '../types';
|
||||
import { centerPointRegistry, MapCenterID } from '../view';
|
||||
|
||||
import { getLayersExtent } from './getLayersExtent';
|
||||
|
||||
export const initViewExtent = (view: View, config: MapViewConfig, layers: Collection<BaseLayer>) => {
|
||||
const v = centerPointRegistry.getIfExists(config.id);
|
||||
if (v) {
|
||||
let coord: Coordinate | undefined = undefined;
|
||||
if (v.lat == null) {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||
} else if (v.id === MapCenterID.Fit) {
|
||||
const extent = getLayersExtent(layers);
|
||||
if (!isEmpty(extent)) {
|
||||
view.fit(extent, {
|
||||
padding: [30, 30, 30, 30],
|
||||
maxZoom: config.zoom ?? config.maxZoom,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO: view requires special handling
|
||||
}
|
||||
} 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 && v?.id !== MapCenterID.Fit) {
|
||||
view.setZoom(config.zoom);
|
||||
}
|
||||
};
|
||||
|
||||
export const initMapView = (
|
||||
config: MapViewConfig,
|
||||
sharedView?: View | undefined,
|
||||
layers?: Collection<BaseLayer>
|
||||
): Array<View | undefined> => {
|
||||
let view = new View({
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
showFullExtent: true, // allows zooming so the full range is visible
|
||||
});
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
if (!sharedView) {
|
||||
sharedView = view;
|
||||
} else {
|
||||
view = sharedView;
|
||||
}
|
||||
}
|
||||
if (layers) {
|
||||
initViewExtent(view, config, layers);
|
||||
}
|
||||
|
||||
return [sharedView, view];
|
||||
};
|
Loading…
Reference in New Issue
Block a user