mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: support multiple layers (#40906)
This commit is contained in:
parent
14225b07b2
commit
45e1765733
@ -66,10 +66,15 @@ export interface MapLayerOptions<TConfig = any> {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface MapLayerHandler {
|
export interface MapLayerHandler<TConfig = any> {
|
||||||
init: () => BaseLayer;
|
init: () => BaseLayer;
|
||||||
update?: (data: PanelData) => void;
|
update?: (data: PanelData) => void;
|
||||||
legend?: ReactNode;
|
legend?: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show custom elements in the panel edit UI
|
||||||
|
*/
|
||||||
|
registerOptionsUI?: (builder: PanelOptionsEditorBuilder<MapLayerOptions<TConfig>>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,9 +103,4 @@ export interface MapLayerRegistryItem<TConfig = MapLayerOptions> extends Registr
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
create: (map: Map, options: MapLayerOptions<TConfig>, theme: GrafanaTheme2) => Promise<MapLayerHandler>;
|
create: (map: Map, options: MapLayerOptions<TConfig>, theme: GrafanaTheme2) => Promise<MapLayerHandler>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Show custom elements in the panel edit UI
|
|
||||||
*/
|
|
||||||
registerOptionsUI?: (builder: PanelOptionsEditorBuilder<MapLayerOptions<TConfig>>) => void;
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
UnitFieldConfigSettings,
|
UnitFieldConfigSettings,
|
||||||
unitOverrideProcessor,
|
unitOverrideProcessor,
|
||||||
FieldNamePickerConfigSettings,
|
FieldNamePickerConfigSettings,
|
||||||
|
StandardEditorContext,
|
||||||
} from '../field';
|
} from '../field';
|
||||||
import { PanelOptionsSupplier } from '../panel/PanelPlugin';
|
import { PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||||
|
|
||||||
@ -133,6 +134,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
|
|||||||
export interface NestedValueAccess {
|
export interface NestedValueAccess {
|
||||||
getValue: (path: string) => any;
|
getValue: (path: string) => any;
|
||||||
onChange: (path: string, value: any) => void;
|
onChange: (path: string, value: any) => void;
|
||||||
|
getContext?: (parent: StandardEditorContext<any, any>) => StandardEditorContext<any, any>;
|
||||||
}
|
}
|
||||||
export interface NestedPanelOptions<TSub = any> {
|
export interface NestedPanelOptions<TSub = any> {
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -136,12 +136,16 @@ export function fillOptionsPaneItems(
|
|||||||
|
|
||||||
// Nested options get passed up one level
|
// Nested options get passed up one level
|
||||||
if (isNestedPanelOptions(pluginOption)) {
|
if (isNestedPanelOptions(pluginOption)) {
|
||||||
const sub = access.getValue(pluginOption.path);
|
const subAccess = pluginOption.getNestedValueAccess(access);
|
||||||
|
const subContext = subAccess.getContext
|
||||||
|
? subAccess.getContext(context)
|
||||||
|
: { ...context, options: access.getValue(pluginOption.path) };
|
||||||
|
|
||||||
fillOptionsPaneItems(
|
fillOptionsPaneItems(
|
||||||
pluginOption.getBuilder(),
|
pluginOption.getBuilder(),
|
||||||
pluginOption.getNestedValueAccess(access),
|
subAccess,
|
||||||
getOptionsPaneCategory,
|
getOptionsPaneCategory,
|
||||||
{ ...context, options: sub },
|
subContext,
|
||||||
category // parent category
|
category // parent category
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
@ -14,7 +14,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
type Props = StandardEditorProps<any, InstanceState, PanelOptions>;
|
type Props = StandardEditorProps<any, InstanceState, PanelOptions>;
|
||||||
|
|
||||||
export class LayerElementListEditor extends PureComponent<Props> {
|
export class LayerElementListEditor extends PureComponent<Props> {
|
||||||
style = getStyles(config.theme);
|
style = getLayerDragStyles(config.theme);
|
||||||
|
|
||||||
onAddItem = (sel: SelectableValue<string>) => {
|
onAddItem = (sel: SelectableValue<string>) => {
|
||||||
// const reg = drawItemsRegistry.getIfExists(sel.value);
|
// const reg = drawItemsRegistry.getIfExists(sel.value);
|
||||||
@ -162,7 +162,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
export const getLayerDragStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
margin-bottom: ${theme.spacing.md};
|
margin-bottom: ${theme.spacing.md};
|
||||||
`,
|
`,
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import React, { Component, ReactNode } from 'react';
|
import React, { Component, ReactNode } from 'react';
|
||||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry, defaultBaseLayer } from './layers/registry';
|
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
||||||
import { Map, MapBrowserEvent, View } from 'ol';
|
import { Map, MapBrowserEvent, View } from 'ol';
|
||||||
import Attribution from 'ol/control/Attribution';
|
import Attribution from 'ol/control/Attribution';
|
||||||
import Zoom from 'ol/control/Zoom';
|
import Zoom from 'ol/control/Zoom';
|
||||||
import ScaleLine from 'ol/control/ScaleLine';
|
import ScaleLine from 'ol/control/ScaleLine';
|
||||||
import BaseLayer from 'ol/layer/Base';
|
|
||||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PanelData,
|
PanelData,
|
||||||
MapLayerHandler,
|
|
||||||
MapLayerOptions,
|
MapLayerOptions,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
@ -20,7 +18,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
|
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig } from './types';
|
||||||
import { centerPointRegistry, MapCenterID } from './view';
|
import { centerPointRegistry, MapCenterID } from './view';
|
||||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||||
import { Coordinate } from 'ol/coordinate';
|
import { Coordinate } from 'ol/coordinate';
|
||||||
@ -32,12 +30,10 @@ import { getGlobalStyles } from './globalStyles';
|
|||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
|
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
|
||||||
import { DataHoverView } from './components/DataHoverView';
|
import { DataHoverView } from './components/DataHoverView';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
interface MapLayerState {
|
import { PanelEditExitedEvent } from 'app/types/events';
|
||||||
config: MapLayerOptions;
|
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||||
handler: MapLayerHandler;
|
import { cloneDeep } from 'lodash';
|
||||||
layer: BaseLayer; // used to add|remove
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows multiple panels to share the same view instance
|
// Allows multiple panels to share the same view instance
|
||||||
let sharedView: View | undefined = undefined;
|
let sharedView: View | undefined = undefined;
|
||||||
@ -47,31 +43,51 @@ interface State extends OverlayProps {
|
|||||||
ttip?: GeomapHoverPayload;
|
ttip?: GeomapHoverPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GeomapLayerActions {
|
||||||
|
selectLayer: (uid: string) => void;
|
||||||
|
deleteLayer: (uid: string) => void;
|
||||||
|
addlayer: (type: string) => void;
|
||||||
|
reorder: (src: number, dst: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeomapInstanceState {
|
||||||
|
map?: Map;
|
||||||
|
layers: MapLayerState[];
|
||||||
|
selected: number;
|
||||||
|
actions: GeomapLayerActions;
|
||||||
|
}
|
||||||
|
|
||||||
export class GeomapPanel extends Component<Props, State> {
|
export class GeomapPanel extends Component<Props, State> {
|
||||||
static contextType = PanelContextRoot;
|
static contextType = PanelContextRoot;
|
||||||
panelContext: PanelContext = {} as PanelContext;
|
panelContext: PanelContext = {} as PanelContext;
|
||||||
|
private subs = new Subscription();
|
||||||
|
|
||||||
globalCSS = getGlobalStyles(config.theme2);
|
globalCSS = getGlobalStyles(config.theme2);
|
||||||
|
|
||||||
counter = 0;
|
counter = 0;
|
||||||
map?: Map;
|
|
||||||
basemap?: BaseLayer;
|
|
||||||
layers: MapLayerState[] = [];
|
|
||||||
mouseWheelZoom?: MouseWheelZoom;
|
mouseWheelZoom?: MouseWheelZoom;
|
||||||
style = getStyles(config.theme);
|
style = getStyles(config.theme);
|
||||||
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
|
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
|
||||||
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
|
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
|
||||||
|
|
||||||
|
map?: Map;
|
||||||
|
mapDiv?: HTMLDivElement;
|
||||||
|
layers: MapLayerState[] = [];
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {};
|
||||||
|
this.subs.add(
|
||||||
|
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||||
|
if (this.mapDiv && this.props.id === evt.payload) {
|
||||||
|
this.initMapRef(this.mapDiv);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.panelContext = this.context as PanelContext;
|
this.panelContext = this.context as PanelContext;
|
||||||
if (this.panelContext.onInstanceStateChange) {
|
|
||||||
this.panelContext.onInstanceStateChange(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
shouldComponentUpdate(nextProps: Props) {
|
||||||
@ -84,25 +100,94 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.map.updateSize();
|
this.map.updateSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// External configuration changed
|
|
||||||
let layersChanged = false;
|
|
||||||
if (this.props.options !== nextProps.options) {
|
|
||||||
layersChanged = this.optionsChanged(nextProps.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// External data changed
|
// External data changed
|
||||||
if (layersChanged || this.props.data !== nextProps.data) {
|
if (this.props.data !== nextProps.data) {
|
||||||
this.dataChanged(nextProps.data);
|
this.dataChanged(nextProps.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // always?
|
return true; // always?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private doOptionsUpdate(selected: number) {
|
||||||
|
const { options, onOptionsChange } = this.props;
|
||||||
|
const layers = this.layers;
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
basemap: layers[0].options,
|
||||||
|
layers: layers.slice(1).map((v) => v.options),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify the the panel editor
|
||||||
|
if (this.panelContext.onInstanceStateChange) {
|
||||||
|
this.panelContext.onInstanceStateChange({
|
||||||
|
map: this.map,
|
||||||
|
layers: layers,
|
||||||
|
selected,
|
||||||
|
actions: this.actions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions: GeomapLayerActions = {
|
||||||
|
selectLayer: (uid: string) => {
|
||||||
|
const selected = this.layers.findIndex((v) => v.UID === uid);
|
||||||
|
if (this.panelContext.onInstanceStateChange) {
|
||||||
|
this.panelContext.onInstanceStateChange({
|
||||||
|
map: this.map,
|
||||||
|
layers: this.layers,
|
||||||
|
selected,
|
||||||
|
actions: this.actions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteLayer: (uid: string) => {
|
||||||
|
const layers: MapLayerState[] = [];
|
||||||
|
for (const lyr of this.layers) {
|
||||||
|
if (lyr.UID === 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,
|
||||||
|
config: cloneDeep(item.defaultOptions),
|
||||||
|
},
|
||||||
|
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) => {
|
||||||
|
const result = Array.from(this.layers);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
this.layers = result;
|
||||||
|
|
||||||
|
this.doOptionsUpdate(endIndex);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the panel options change
|
* Called when the panel options change
|
||||||
|
*
|
||||||
|
* NOTE: changes to basemap and layers are handled independently
|
||||||
*/
|
*/
|
||||||
optionsChanged(options: GeomapPanelOptions): boolean {
|
optionsChanged(options: GeomapPanelOptions) {
|
||||||
let layersChanged = false;
|
|
||||||
const oldOptions = this.props.options;
|
const oldOptions = this.props.options;
|
||||||
console.log('options changed!', options);
|
console.log('options changed!', options);
|
||||||
|
|
||||||
@ -115,19 +200,6 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
console.log('Controls changed');
|
console.log('Controls changed');
|
||||||
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
|
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 ?? []); // async
|
|
||||||
layersChanged = true;
|
|
||||||
}
|
|
||||||
return layersChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,6 +214,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initMapRef = async (div: HTMLDivElement) => {
|
initMapRef = async (div: HTMLDivElement) => {
|
||||||
|
this.mapDiv = div;
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.dispose();
|
this.map.dispose();
|
||||||
}
|
}
|
||||||
@ -151,7 +224,8 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
this.map = new Map({
|
|
||||||
|
const map = (this.map = new Map({
|
||||||
view: this.initMapView(options.view),
|
view: this.initMapView(options.view),
|
||||||
pixelRatio: 1, // or zoom?
|
pixelRatio: 1, // or zoom?
|
||||||
layers: [], // loaded explicitly below
|
layers: [], // loaded explicitly below
|
||||||
@ -160,12 +234,33 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
interactions: interactionDefaults({
|
interactions: interactionDefaults({
|
||||||
mouseWheelZoom: false, // managed by initControls
|
mouseWheelZoom: false, // managed by initControls
|
||||||
}),
|
}),
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
const layers: MapLayerState[] = [];
|
||||||
|
try {
|
||||||
|
layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||||
|
|
||||||
|
// Default layer values
|
||||||
|
let layerOptions = options.layers;
|
||||||
|
if (!layerOptions) {
|
||||||
|
layerOptions = [defaultMarkersConfig];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lyr of layerOptions) {
|
||||||
|
layers.push(await this.initLayer(map, lyr, false));
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('error loading layers', ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layers = layers;
|
||||||
|
for (const lyr of layers) {
|
||||||
|
this.map.addLayer(lyr.layer);
|
||||||
|
}
|
||||||
|
|
||||||
this.mouseWheelZoom = new MouseWheelZoom();
|
this.mouseWheelZoom = new MouseWheelZoom();
|
||||||
this.map.addInteraction(this.mouseWheelZoom);
|
this.map.addInteraction(this.mouseWheelZoom);
|
||||||
this.initControls(options.controls);
|
this.initControls(options.controls);
|
||||||
this.initBasemap(options.basemap);
|
|
||||||
await this.initLayers(options.layers);
|
|
||||||
this.forceUpdate(); // first render
|
this.forceUpdate(); // first render
|
||||||
|
|
||||||
// Tooltip listener
|
// Tooltip listener
|
||||||
@ -173,6 +268,22 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||||
this.props.eventBus.publish(new DataHoverClearEvent());
|
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify the 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clearTooltip = () => {
|
||||||
|
if (this.state.ttip) {
|
||||||
|
this.setState({ ttip: undefined });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||||
@ -223,63 +334,93 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async initBasemap(cfg: MapLayerOptions) {
|
private updateLayer = async (uid: string, newOptions: MapLayerOptions): Promise<boolean> => {
|
||||||
if (!this.map) {
|
if (!this.map) {
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
const selected = this.layers.findIndex((v) => v.UID === uid);
|
||||||
|
if (selected < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const layers = this.layers.slice(0);
|
||||||
|
try {
|
||||||
|
let found = false;
|
||||||
|
const current = this.layers[selected];
|
||||||
|
const info = await this.initLayer(this.map, newOptions, current.isBasemap);
|
||||||
|
const group = this.map?.getLayers()!;
|
||||||
|
for (let i = 0; i < group?.getLength(); i++) {
|
||||||
|
if (group.item(i) === current.layer) {
|
||||||
|
found = true;
|
||||||
|
group.setAt(i, info.layer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
console.warn('ERROR not found', uid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
layers[selected] = info;
|
||||||
|
|
||||||
|
// initalize with new data
|
||||||
|
if (info.handler.update) {
|
||||||
|
info.handler.update(this.props.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('ERROR', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// validate names, basemap etc
|
||||||
|
|
||||||
|
this.layers = layers;
|
||||||
|
this.doOptionsUpdate(selected);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
async initLayer(map: Map, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
|
||||||
|
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||||
|
options = DEFAULT_BASEMAP_CONFIG;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cfg?.type || config.geomapDisableCustomBaseLayer) {
|
// Use default makers layer
|
||||||
cfg = DEFAULT_BASEMAP_CONFIG;
|
if (!options?.type) {
|
||||||
|
options = {
|
||||||
|
type: MARKERS_LAYER_ID,
|
||||||
|
config: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const item = geomapLayerRegistry.getIfExists(cfg.type) ?? defaultBaseLayer;
|
|
||||||
const handler = await item.create(this.map, cfg, config.theme2);
|
const item = geomapLayerRegistry.getIfExists(options.type);
|
||||||
|
if (!item) {
|
||||||
|
return Promise.reject('unknown layer: ' + options.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = await item.create(map, options, config.theme2);
|
||||||
const layer = handler.init();
|
const layer = handler.init();
|
||||||
if (this.basemap) {
|
|
||||||
this.map.removeLayer(this.basemap);
|
|
||||||
this.basemap.dispose();
|
|
||||||
}
|
|
||||||
this.basemap = layer;
|
|
||||||
this.map.getLayers().insertAt(0, this.basemap!);
|
|
||||||
}
|
|
||||||
|
|
||||||
async initLayers(layers: MapLayerOptions[]) {
|
// const key = layer.on('change', () => {
|
||||||
// 1st remove existing layers
|
// const state = layer.getLayerState();
|
||||||
for (const state of this.layers) {
|
// console.log('LAYER', key, state);
|
||||||
this.map!.removeLayer(state.layer);
|
// });
|
||||||
state.layer.dispose();
|
|
||||||
|
if (handler.update) {
|
||||||
|
handler.update(this.props.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!layers) {
|
const UID = `lyr-${this.counter++}`;
|
||||||
layers = [];
|
return {
|
||||||
}
|
UID,
|
||||||
|
isBasemap,
|
||||||
|
options,
|
||||||
|
layer,
|
||||||
|
handler,
|
||||||
|
|
||||||
const legends: React.ReactNode[] = [];
|
// Used by the editors
|
||||||
this.layers = [];
|
onChange: (cfg) => {
|
||||||
for (const overlay of layers) {
|
this.updateLayer(UID, cfg);
|
||||||
const item = geomapLayerRegistry.getIfExists(overlay.type);
|
},
|
||||||
if (!item) {
|
};
|
||||||
console.warn('unknown layer type: ', overlay);
|
|
||||||
continue; // TODO -- panel warning?
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = await item.create(this.map!, overlay, config.theme2);
|
|
||||||
const layer = handler.init();
|
|
||||||
(layer as any).___handler = handler;
|
|
||||||
this.map!.addLayer(layer);
|
|
||||||
this.layers.push({
|
|
||||||
config: overlay,
|
|
||||||
layer,
|
|
||||||
handler,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (handler.legend) {
|
|
||||||
legends.push(<div key={`${this.counter++}`}>{handler.legend}</div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({ bottomLeft: legends });
|
|
||||||
|
|
||||||
// Update data after init layers
|
|
||||||
this.dataChanged(this.props.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initMapView(config: MapViewConfig): View {
|
initMapView(config: MapViewConfig): View {
|
||||||
@ -367,7 +508,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Global styles={this.globalCSS} />
|
<Global styles={this.globalCSS} />
|
||||||
<div className={this.style.wrap}>
|
<div className={this.style.wrap} onMouseLeave={this.clearTooltip}>
|
||||||
<div className={this.style.map} ref={this.initMapRef}></div>
|
<div className={this.style.map} ref={this.initMapRef}></div>
|
||||||
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
|
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
|
||||||
</div>
|
</div>
|
||||||
|
126
public/app/plugins/panel/geomap/editor/LayersEditor.tsx
Normal file
126
public/app/plugins/panel/geomap/editor/LayersEditor.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { Container, Icon, IconButton, ValuePicker } from '@grafana/ui';
|
||||||
|
import { StandardEditorProps } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { GeomapPanelOptions } from '../types';
|
||||||
|
import { GeomapInstanceState } from '../GeomapPanel';
|
||||||
|
import { geomapLayerRegistry } from '../layers/registry';
|
||||||
|
import { getLayerDragStyles } from '../../canvas/editor/LayerElementListEditor';
|
||||||
|
import { dataLayerFilter } from './layerEditor';
|
||||||
|
|
||||||
|
type Props = StandardEditorProps<any, any, GeomapPanelOptions, GeomapInstanceState>;
|
||||||
|
|
||||||
|
export class LayersEditor extends PureComponent<Props> {
|
||||||
|
style = getLayerDragStyles(config.theme);
|
||||||
|
|
||||||
|
getRowStyle = (sel: boolean) => {
|
||||||
|
return sel ? `${this.style.row} ${this.style.sel}` : this.style.row;
|
||||||
|
};
|
||||||
|
|
||||||
|
onDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { layers, actions } = this.props.context.instanceState ?? {};
|
||||||
|
if (!layers || !actions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// account for the reverse order and offset (0 is baselayer)
|
||||||
|
const count = layers.length - 1;
|
||||||
|
const src = (result.source.index - count) * -1;
|
||||||
|
const dst = (result.destination.index - count) * -1;
|
||||||
|
|
||||||
|
actions.reorder(src, dst);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { layers, selected, actions } = this.props.context.instanceState ?? {};
|
||||||
|
if (!layers || !actions) {
|
||||||
|
return <div>No layers?</div>;
|
||||||
|
}
|
||||||
|
const baselayer = layers[0];
|
||||||
|
|
||||||
|
const styles = this.style;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<ValuePicker
|
||||||
|
icon="plus"
|
||||||
|
label="Add layer"
|
||||||
|
variant="secondary"
|
||||||
|
options={geomapLayerRegistry.selectOptions(undefined, dataLayerFilter).options}
|
||||||
|
onChange={(v) => actions.addlayer(v.value!)}
|
||||||
|
isFullWidth={true}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<DragDropContext onDragEnd={this.onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{(() => {
|
||||||
|
// reverse order
|
||||||
|
const rows: any = [];
|
||||||
|
for (let i = layers.length - 1; i > 0; i--) {
|
||||||
|
const element = layers[i];
|
||||||
|
rows.push(
|
||||||
|
<Draggable key={element.UID} draggableId={element.UID} index={rows.length}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={this.getRowStyle(i === selected)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
onMouseDown={() => actions!.selectLayer(element.UID)}
|
||||||
|
>
|
||||||
|
<span className={styles.typeWrapper}>{element.options.type}</span>
|
||||||
|
<div className={styles.textWrapper}> ({element.layer.getSourceState() ?? '?'})</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={cx(styles.actionIcon, styles.dragIcon)}
|
||||||
|
onClick={() => actions.deleteLayer(element.UID)}
|
||||||
|
surface="header"
|
||||||
|
/>
|
||||||
|
{layers.length > 2 && (
|
||||||
|
<Icon
|
||||||
|
title="Drag and drop to reorder"
|
||||||
|
name="draggabledots"
|
||||||
|
size="lg"
|
||||||
|
className={styles.dragIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
|
||||||
|
{false && baselayer && (
|
||||||
|
<>
|
||||||
|
<label>Base layer</label>
|
||||||
|
<div className={this.getRowStyle(false)}>
|
||||||
|
<span className={styles.typeWrapper}>{baselayer.options.type}</span>
|
||||||
|
<div className={styles.textWrapper}> {baselayer.UID}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,9 @@ import { GeomapPanelOptions, MapViewConfig } from '../types';
|
|||||||
import { centerPointRegistry, MapCenterID } from '../view';
|
import { centerPointRegistry, MapCenterID } from '../view';
|
||||||
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||||
import { toLonLat } from 'ol/proj';
|
import { toLonLat } from 'ol/proj';
|
||||||
import { GeomapPanel } from '../GeomapPanel';
|
import { GeomapInstanceState } from '../GeomapPanel';
|
||||||
|
|
||||||
export const MapViewEditor: FC<StandardEditorProps<MapViewConfig, any, GeomapPanelOptions, GeomapPanel>> = ({
|
export const MapViewEditor: FC<StandardEditorProps<MapViewConfig, any, GeomapPanelOptions, GeomapInstanceState>> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
context,
|
context,
|
||||||
|
@ -11,38 +11,51 @@ import { GazetteerPathEditor } from './GazetteerPathEditor';
|
|||||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||||
import { defaultMarkersConfig } from '../layers/data/markersLayer';
|
import { defaultMarkersConfig } from '../layers/data/markersLayer';
|
||||||
import { hasAlphaPanels } from 'app/core/config';
|
import { hasAlphaPanels } from 'app/core/config';
|
||||||
|
import { MapLayerState } from '../types';
|
||||||
|
import { get as lodashGet } from 'lodash';
|
||||||
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
|
|
||||||
export interface LayerEditorOptions {
|
export interface LayerEditorOptions {
|
||||||
|
state: MapLayerState;
|
||||||
category: string[];
|
category: string[];
|
||||||
path: string;
|
|
||||||
basemaps: boolean; // only basemaps
|
basemaps: boolean; // only basemaps
|
||||||
current?: MapLayerOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<MapLayerOptions> {
|
export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<MapLayerOptions> {
|
||||||
return {
|
return {
|
||||||
category: opts.category,
|
category: opts.category,
|
||||||
path: opts.path,
|
path: '--', // Not used
|
||||||
defaultValue: opts.basemaps ? DEFAULT_BASEMAP_CONFIG : defaultMarkersConfig,
|
defaultValue: opts.basemaps ? DEFAULT_BASEMAP_CONFIG : defaultMarkersConfig,
|
||||||
values: (parent: NestedValueAccess) => ({
|
values: (parent: NestedValueAccess) => ({
|
||||||
getValue: (path: string) => parent.getValue(`${opts.path}.${path}`),
|
getContext: (parent) => {
|
||||||
|
return { ...parent, options: opts.state.options, instanceState: opts.state };
|
||||||
|
},
|
||||||
|
getValue: (path: string) => lodashGet(opts.state.options, path),
|
||||||
onChange: (path: string, value: any) => {
|
onChange: (path: string, value: any) => {
|
||||||
|
const { state } = opts;
|
||||||
|
const { options } = state;
|
||||||
if (path === 'type' && value) {
|
if (path === 'type' && value) {
|
||||||
const layer = geomapLayerRegistry.getIfExists(value);
|
const layer = geomapLayerRegistry.getIfExists(value);
|
||||||
if (layer) {
|
if (layer) {
|
||||||
parent.onChange(opts.path, {
|
console.log('Change layer type:', value, state);
|
||||||
...opts.current, // keep current shared options
|
state.onChange({
|
||||||
|
...options, // keep current shared options
|
||||||
type: layer.id,
|
type: layer.id,
|
||||||
config: { ...layer.defaultOptions }, // clone?
|
config: { ...layer.defaultOptions }, // clone?
|
||||||
});
|
});
|
||||||
return; // reset current values
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent.onChange(`${opts.path}.${path}`, value);
|
state.onChange(setOptionImmutably(options, path, value));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
build: (builder, context) => {
|
build: (builder, context) => {
|
||||||
const { options } = context;
|
if (!opts.state) {
|
||||||
|
console.log('MISSING LAYER!!!', opts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handler, options } = opts.state;
|
||||||
const layer = geomapLayerRegistry.getIfExists(options?.type);
|
const layer = geomapLayerRegistry.getIfExists(options?.type);
|
||||||
|
|
||||||
const layerTypes = geomapLayerRegistry.selectOptions(
|
const layerTypes = geomapLayerRegistry.selectOptions(
|
||||||
@ -54,81 +67,88 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
|
|||||||
|
|
||||||
builder.addSelect({
|
builder.addSelect({
|
||||||
path: 'type',
|
path: 'type',
|
||||||
name: undefined as any, // required, but hide space
|
name: 'Layer type', // required, but hide space
|
||||||
settings: {
|
settings: {
|
||||||
options: layerTypes.options,
|
options: layerTypes.options,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (layer) {
|
if (!layer) {
|
||||||
if (layer.showLocation) {
|
return; // unknown layer type
|
||||||
builder
|
}
|
||||||
.addRadio({
|
|
||||||
path: 'location.mode',
|
// Don't show UI for default configuration
|
||||||
name: 'Location',
|
if (options.type === DEFAULT_BASEMAP_CONFIG.type) {
|
||||||
description: '',
|
return;
|
||||||
defaultValue: FrameGeometrySourceMode.Auto,
|
}
|
||||||
settings: {
|
|
||||||
options: [
|
if (layer.showLocation) {
|
||||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
|
builder
|
||||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
|
.addRadio({
|
||||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
|
path: 'location.mode',
|
||||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
|
name: 'Location',
|
||||||
],
|
description: '',
|
||||||
},
|
defaultValue: FrameGeometrySourceMode.Auto,
|
||||||
})
|
settings: {
|
||||||
.addFieldNamePicker({
|
options: [
|
||||||
path: 'location.latitude',
|
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
|
||||||
name: 'Latitude field',
|
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
|
||||||
settings: {
|
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
|
||||||
filter: (f: Field) => f.type === FieldType.number,
|
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
|
||||||
noFieldsMessage: 'No numeric fields found',
|
],
|
||||||
},
|
},
|
||||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
})
|
||||||
})
|
.addFieldNamePicker({
|
||||||
.addFieldNamePicker({
|
path: 'location.latitude',
|
||||||
path: 'location.longitude',
|
name: 'Latitude field',
|
||||||
name: 'Longitude field',
|
settings: {
|
||||||
settings: {
|
filter: (f: Field) => f.type === FieldType.number,
|
||||||
filter: (f: Field) => f.type === FieldType.number,
|
noFieldsMessage: 'No numeric fields found',
|
||||||
noFieldsMessage: 'No numeric fields found',
|
},
|
||||||
},
|
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
})
|
||||||
})
|
.addFieldNamePicker({
|
||||||
.addFieldNamePicker({
|
path: 'location.longitude',
|
||||||
path: 'location.geohash',
|
name: 'Longitude field',
|
||||||
name: 'Geohash field',
|
settings: {
|
||||||
settings: {
|
filter: (f: Field) => f.type === FieldType.number,
|
||||||
filter: (f: Field) => f.type === FieldType.string,
|
noFieldsMessage: 'No numeric fields found',
|
||||||
noFieldsMessage: 'No strings fields found',
|
},
|
||||||
},
|
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash,
|
})
|
||||||
// eslint-disable-next-line react/display-name
|
.addFieldNamePicker({
|
||||||
// info: (props) => <div>HELLO</div>,
|
path: 'location.geohash',
|
||||||
})
|
name: 'Geohash field',
|
||||||
.addFieldNamePicker({
|
settings: {
|
||||||
path: 'location.lookup',
|
filter: (f: Field) => f.type === FieldType.string,
|
||||||
name: 'Lookup field',
|
noFieldsMessage: 'No strings fields found',
|
||||||
settings: {
|
},
|
||||||
filter: (f: Field) => f.type === FieldType.string,
|
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash,
|
||||||
noFieldsMessage: 'No strings fields found',
|
// eslint-disable-next-line react/display-name
|
||||||
},
|
// info: (props) => <div>HELLO</div>,
|
||||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
})
|
||||||
})
|
.addFieldNamePicker({
|
||||||
.addCustomEditor({
|
path: 'location.lookup',
|
||||||
id: 'gazetteer',
|
name: 'Lookup field',
|
||||||
path: 'location.gazetteer',
|
settings: {
|
||||||
name: 'Gazetteer',
|
filter: (f: Field) => f.type === FieldType.string,
|
||||||
editor: GazetteerPathEditor,
|
noFieldsMessage: 'No strings fields found',
|
||||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
},
|
||||||
});
|
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
||||||
}
|
})
|
||||||
if (layer.registerOptionsUI) {
|
.addCustomEditor({
|
||||||
layer.registerOptionsUI(builder);
|
id: 'gazetteer',
|
||||||
}
|
path: 'location.gazetteer',
|
||||||
if (layer.showOpacity) {
|
name: 'Gazetteer',
|
||||||
// TODO -- add opacity check
|
editor: GazetteerPathEditor,
|
||||||
}
|
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (handler.registerOptionsUI) {
|
||||||
|
handler.registerOptionsUI(builder);
|
||||||
|
}
|
||||||
|
if (layer.showOpacity) {
|
||||||
|
// TODO -- add opacity check
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -144,7 +164,7 @@ function baseMapFilter(layer: MapLayerRegistryItem): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dataLayerFilter(layer: MapLayerRegistryItem): boolean {
|
export function dataLayerFilter(layer: MapLayerRegistryItem): boolean {
|
||||||
if (layer.isBaseMap) {
|
if (layer.isBaseMap) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -50,29 +50,29 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
|
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'config.theme',
|
path: 'config.theme',
|
||||||
name: 'Theme',
|
name: 'Theme',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
|
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
|
||||||
{ value: LayerTheme.Light, label: 'Light' },
|
{ value: LayerTheme.Light, label: 'Light' },
|
||||||
{ value: LayerTheme.Dark, label: 'Dark' },
|
{ value: LayerTheme.Dark, label: 'Dark' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
defaultValue: defaultCartoConfig.theme!,
|
defaultValue: defaultCartoConfig.theme!,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'config.showLabels',
|
path: 'config.showLabels',
|
||||||
name: 'Show labels',
|
name: 'Show labels',
|
||||||
description: '',
|
description: '',
|
||||||
defaultValue: defaultCartoConfig.showLabels,
|
defaultValue: defaultCartoConfig.showLabels,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cartoLayers = [carto];
|
export const cartoLayers = [carto];
|
||||||
|
@ -66,35 +66,36 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
|||||||
cfg.attribution = `Tiles © <a href="${base}${svc.slug}/MapServer">ArcGIS</a>`;
|
cfg.attribution = `Tiles © <a href="${base}${svc.slug}/MapServer">ArcGIS</a>`;
|
||||||
}
|
}
|
||||||
const opts = { ...options, config: cfg as XYZConfig };
|
const opts = { ...options, config: cfg as XYZConfig };
|
||||||
return xyzTiles.create(map, opts, theme);
|
return xyzTiles.create(map, opts, theme).then((xyz) => {
|
||||||
},
|
xyz.registerOptionsUI = (builder) => {
|
||||||
|
builder
|
||||||
registerOptionsUI: (builder) => {
|
.addSelect({
|
||||||
builder
|
path: 'config.server',
|
||||||
.addSelect({
|
name: 'Server instance',
|
||||||
path: 'config.server',
|
settings: {
|
||||||
name: 'Server instance',
|
options: publicServiceRegistry.selectOptions().options,
|
||||||
settings: {
|
},
|
||||||
options: publicServiceRegistry.selectOptions().options,
|
})
|
||||||
},
|
.addTextInput({
|
||||||
})
|
path: 'config.url',
|
||||||
.addTextInput({
|
name: 'URL template',
|
||||||
path: 'config.url',
|
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||||
name: 'URL template',
|
settings: {
|
||||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
placeholder: defaultXYZConfig.url,
|
||||||
settings: {
|
},
|
||||||
placeholder: defaultXYZConfig.url,
|
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||||
},
|
})
|
||||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
.addTextInput({
|
||||||
})
|
path: 'config.attribution',
|
||||||
.addTextInput({
|
name: 'Attribution',
|
||||||
path: 'config.attribution',
|
settings: {
|
||||||
name: 'Attribution',
|
placeholder: defaultXYZConfig.attribution,
|
||||||
settings: {
|
},
|
||||||
placeholder: defaultXYZConfig.attribution,
|
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||||
},
|
});
|
||||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
};
|
||||||
});
|
return xyz;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
@ -37,26 +37,25 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
|||||||
maxZoom: cfg.maxZoom,
|
maxZoom: cfg.maxZoom,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
builder
|
||||||
|
.addTextInput({
|
||||||
|
path: 'config.url',
|
||||||
|
name: 'URL template',
|
||||||
|
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||||
|
settings: {
|
||||||
|
placeholder: defaultXYZConfig.url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addTextInput({
|
||||||
|
path: 'config.attribution',
|
||||||
|
name: 'Attribution',
|
||||||
|
settings: {
|
||||||
|
placeholder: defaultXYZConfig.attribution,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
registerOptionsUI: (builder) => {
|
|
||||||
builder
|
|
||||||
.addTextInput({
|
|
||||||
path: 'config.url',
|
|
||||||
name: 'URL template',
|
|
||||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
|
||||||
settings: {
|
|
||||||
placeholder: defaultXYZConfig.url,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addTextInput({
|
|
||||||
path: 'config.attribution',
|
|
||||||
name: 'Attribution',
|
|
||||||
settings: {
|
|
||||||
placeholder: defaultXYZConfig.attribution,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const genericLayers = [xyzTiles];
|
export const genericLayers = [xyzTiles];
|
||||||
|
@ -3,6 +3,7 @@ import Map from 'ol/Map';
|
|||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
import VectorSource from 'ol/source/Vector';
|
import VectorSource from 'ol/source/Vector';
|
||||||
import GeoJSON from 'ol/format/GeoJSON';
|
import GeoJSON from 'ol/format/GeoJSON';
|
||||||
|
import { unByKey } from 'ol/Observable';
|
||||||
import { Feature } from 'ol';
|
import { Feature } from 'ol';
|
||||||
import { Geometry } from 'ol/geom';
|
import { Geometry } from 'ol/geom';
|
||||||
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
|
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
|
||||||
@ -53,6 +54,19 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
format: new GeoJSON(),
|
format: new GeoJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const key = source.on('change', () => {
|
||||||
|
if (source.getState() == 'ready') {
|
||||||
|
unByKey(key);
|
||||||
|
// var olFeatures = source.getFeatures(); // olFeatures.length === 1
|
||||||
|
// window.setTimeout(function () {
|
||||||
|
// var olFeatures = source.getFeatures(); // olFeatures.length > 1
|
||||||
|
// // Only after using setTimeout can I search the feature list... :(
|
||||||
|
// }, 100)
|
||||||
|
|
||||||
|
console.log('SOURCE READY!!!', source.getFeatures().length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const defaultStyle = new Style({
|
const defaultStyle = new Style({
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: DEFAULT_STYLE_RULE.fillColor,
|
color: DEFAULT_STYLE_RULE.fillColor,
|
||||||
@ -80,37 +94,40 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
|||||||
update: (data: PanelData) => {
|
update: (data: PanelData) => {
|
||||||
console.log('todo... find values matching the ID and update');
|
console.log('todo... find values matching the ID and update');
|
||||||
|
|
||||||
// Update each feature
|
// // Update each feature
|
||||||
source.getFeatures().forEach((f) => {
|
// source.getFeatures().forEach((f) => {
|
||||||
console.log('Find: ', f.getId(), f.getProperties());
|
// console.log('Find: ', f.getId(), f.getProperties());
|
||||||
});
|
// });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Geojson source url
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
const features = source.getFeatures();
|
||||||
|
console.log('FEATURES', source.getState(), features.length, options);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.addSelect({
|
||||||
|
path: 'config.src',
|
||||||
|
name: 'GeoJSON URL',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
|
||||||
|
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
|
||||||
|
],
|
||||||
|
allowCustomValue: true,
|
||||||
|
},
|
||||||
|
defaultValue: defaultOptions.src,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.styles',
|
||||||
|
path: 'config.styles',
|
||||||
|
name: 'Style Rules',
|
||||||
|
editor: GeomapStyleRulesEditor,
|
||||||
|
settings: {},
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Geojson source url
|
|
||||||
registerOptionsUI: (builder) => {
|
|
||||||
builder
|
|
||||||
.addSelect({
|
|
||||||
path: 'config.src',
|
|
||||||
name: 'GeoJSON URL',
|
|
||||||
settings: {
|
|
||||||
options: [
|
|
||||||
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
|
|
||||||
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
|
|
||||||
],
|
|
||||||
allowCustomValue: true,
|
|
||||||
},
|
|
||||||
defaultValue: defaultOptions.src,
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.styles',
|
|
||||||
path: 'config.styles',
|
|
||||||
name: 'Style Rules',
|
|
||||||
editor: GeomapStyleRulesEditor,
|
|
||||||
settings: {},
|
|
||||||
defaultValue: [],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
};
|
};
|
||||||
|
@ -110,52 +110,53 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
|||||||
}
|
}
|
||||||
vectorLayer.setGradient(colors);
|
vectorLayer.setGradient(colors);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Heatmap overlay options
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.weight',
|
||||||
|
path: 'config.weight',
|
||||||
|
name: 'Weight values',
|
||||||
|
description: 'Scale the distribution for each row',
|
||||||
|
editor: ScaleDimensionEditor,
|
||||||
|
settings: {
|
||||||
|
min: 0, // no contribution
|
||||||
|
max: 1,
|
||||||
|
hideRange: true, // Don't show the scale factor
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
// Configured values
|
||||||
|
fixed: 1,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'config.radius',
|
||||||
|
description: 'configures the size of clusters',
|
||||||
|
name: 'Radius',
|
||||||
|
defaultValue: defaultOptions.radius,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'config.blur',
|
||||||
|
description: 'configures the amount of blur of clusters',
|
||||||
|
name: 'Blur',
|
||||||
|
defaultValue: defaultOptions.blur,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Heatmap overlay options
|
|
||||||
registerOptionsUI: (builder) => {
|
|
||||||
builder
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.weight',
|
|
||||||
path: 'config.weight',
|
|
||||||
name: 'Weight values',
|
|
||||||
description: 'Scale the distribution for each row',
|
|
||||||
editor: ScaleDimensionEditor,
|
|
||||||
settings: {
|
|
||||||
min: 0, // no contribution
|
|
||||||
max: 1,
|
|
||||||
hideRange: true, // Don't show the scale factor
|
|
||||||
},
|
|
||||||
defaultValue: {
|
|
||||||
// Configured values
|
|
||||||
fixed: 1,
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'config.radius',
|
|
||||||
description: 'configures the size of clusters',
|
|
||||||
name: 'Radius',
|
|
||||||
defaultValue: defaultOptions.radius,
|
|
||||||
settings: {
|
|
||||||
min: 1,
|
|
||||||
max: 50,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'config.blur',
|
|
||||||
description: 'configures the amount of blur of clusters',
|
|
||||||
name: 'Blur',
|
|
||||||
defaultValue: defaultOptions.blur,
|
|
||||||
settings: {
|
|
||||||
min: 1,
|
|
||||||
max: 50,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// fill in the default values
|
// fill in the default values
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
};
|
};
|
||||||
|
@ -165,67 +165,68 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
const vectorSource = new source.Vector({ features });
|
const vectorSource = new source.Vector({ features });
|
||||||
vectorLayer.setSource(vectorSource);
|
vectorLayer.setSource(vectorSource);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Marker overlay options
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.size',
|
||||||
|
path: 'config.size',
|
||||||
|
name: 'Marker Size',
|
||||||
|
editor: ScaleDimensionEditor,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 100, // possible in the UI
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
// Configured values
|
||||||
|
fixed: DEFAULT_SIZE,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.markerSymbol',
|
||||||
|
path: 'config.markerSymbol',
|
||||||
|
name: 'Marker Symbol',
|
||||||
|
editor: ResourceDimensionEditor,
|
||||||
|
defaultValue: defaultOptions.markerSymbol,
|
||||||
|
settings: {
|
||||||
|
resourceType: 'icon',
|
||||||
|
showSourceRadio: false,
|
||||||
|
folderName: ResourceFolderName.Marker,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.color',
|
||||||
|
path: 'config.color',
|
||||||
|
name: 'Marker Color',
|
||||||
|
editor: ColorDimensionEditor,
|
||||||
|
settings: {},
|
||||||
|
defaultValue: {
|
||||||
|
// Configured values
|
||||||
|
fixed: 'grey',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'config.fillOpacity',
|
||||||
|
name: 'Fill opacity',
|
||||||
|
defaultValue: defaultOptions.fillOpacity,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addBooleanSwitch({
|
||||||
|
path: 'config.showLegend',
|
||||||
|
name: 'Show legend',
|
||||||
|
description: 'Show legend',
|
||||||
|
defaultValue: defaultOptions.showLegend,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Marker overlay options
|
|
||||||
registerOptionsUI: (builder) => {
|
|
||||||
builder
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.size',
|
|
||||||
path: 'config.size',
|
|
||||||
name: 'Marker Size',
|
|
||||||
editor: ScaleDimensionEditor,
|
|
||||||
settings: {
|
|
||||||
min: 1,
|
|
||||||
max: 100, // possible in the UI
|
|
||||||
},
|
|
||||||
defaultValue: {
|
|
||||||
// Configured values
|
|
||||||
fixed: DEFAULT_SIZE,
|
|
||||||
min: 1,
|
|
||||||
max: 20,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.markerSymbol',
|
|
||||||
path: 'config.markerSymbol',
|
|
||||||
name: 'Marker Symbol',
|
|
||||||
editor: ResourceDimensionEditor,
|
|
||||||
defaultValue: defaultOptions.markerSymbol,
|
|
||||||
settings: {
|
|
||||||
resourceType: 'icon',
|
|
||||||
showSourceRadio: false,
|
|
||||||
folderName: ResourceFolderName.Marker,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.color',
|
|
||||||
path: 'config.color',
|
|
||||||
name: 'Marker Color',
|
|
||||||
editor: ColorDimensionEditor,
|
|
||||||
settings: {},
|
|
||||||
defaultValue: {
|
|
||||||
// Configured values
|
|
||||||
fixed: 'grey',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'config.fillOpacity',
|
|
||||||
name: 'Fill opacity',
|
|
||||||
defaultValue: defaultOptions.fillOpacity,
|
|
||||||
settings: {
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addBooleanSwitch({
|
|
||||||
path: 'config.showLegend',
|
|
||||||
name: 'Show legend',
|
|
||||||
description: 'Show legend',
|
|
||||||
defaultValue: defaultOptions.showLegend,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// fill in the default values
|
// fill in the default values
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
|
@ -119,44 +119,44 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
|||||||
const vectorSource = new source.Vector({ features });
|
const vectorSource = new source.Vector({ features });
|
||||||
vectorLayer.setSource(vectorSource);
|
vectorLayer.setSource(vectorSource);
|
||||||
},
|
},
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.labelText',
|
||||||
|
name: 'Text label',
|
||||||
|
path: 'config.labelText',
|
||||||
|
editor: TextDimensionEditor,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.color',
|
||||||
|
path: 'config.color',
|
||||||
|
name: 'Text color',
|
||||||
|
editor: ColorDimensionEditor,
|
||||||
|
settings: {},
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'config.fillOpacity',
|
||||||
|
name: 'Text opacity',
|
||||||
|
defaultValue: defaultOptions.fillOpacity,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.fontSize',
|
||||||
|
path: 'config.fontSize',
|
||||||
|
name: 'Text size',
|
||||||
|
editor: ScaleDimensionEditor,
|
||||||
|
settings: {
|
||||||
|
fixed: defaultOptions.fontSize.fixed,
|
||||||
|
min: defaultOptions.fontSize.min,
|
||||||
|
max: defaultOptions.fontSize.max,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
registerOptionsUI: (builder) => {
|
|
||||||
builder
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.labelText',
|
|
||||||
name: 'Text label',
|
|
||||||
path: 'config.labelText',
|
|
||||||
editor: TextDimensionEditor,
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.color',
|
|
||||||
path: 'config.color',
|
|
||||||
name: 'Text color',
|
|
||||||
editor: ColorDimensionEditor,
|
|
||||||
settings: {},
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
|
||||||
path: 'config.fillOpacity',
|
|
||||||
name: 'Text opacity',
|
|
||||||
defaultValue: defaultOptions.fillOpacity,
|
|
||||||
settings: {
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
id: 'config.fontSize',
|
|
||||||
path: 'config.fontSize',
|
|
||||||
name: 'Text size',
|
|
||||||
editor: ScaleDimensionEditor,
|
|
||||||
settings: {
|
|
||||||
fixed: defaultOptions.fontSize.fixed,
|
|
||||||
min: defaultOptions.fontSize.min,
|
|
||||||
max: defaultOptions.fontSize.max,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
import { GeomapPanel } from './GeomapPanel';
|
import { GeomapInstanceState, GeomapPanel } from './GeomapPanel';
|
||||||
import { MapViewEditor } from './editor/MapViewEditor';
|
import { MapViewEditor } from './editor/MapViewEditor';
|
||||||
import { defaultView, GeomapPanelOptions } from './types';
|
import { defaultView, GeomapPanelOptions } from './types';
|
||||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||||
import { getLayerEditor } from './editor/layerEditor';
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
|
import { LayersEditor } from './editor/LayersEditor';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||||
@ -32,45 +33,48 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
|||||||
defaultValue: defaultView.shared,
|
defaultValue: defaultView.shared,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check server settings to disable custom basemap settings
|
const state = context.instanceState as GeomapInstanceState;
|
||||||
if (config.geomapDisableCustomBaseLayer) {
|
if (!state?.layers) {
|
||||||
|
// TODO? show spinner?
|
||||||
|
} else {
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
category: ['Base layer'],
|
category: ['Data layer'],
|
||||||
id: 'layers',
|
id: 'layers',
|
||||||
path: '',
|
path: '',
|
||||||
name: '',
|
name: '',
|
||||||
// eslint-disable-next-line react/display-name
|
editor: LayersEditor,
|
||||||
editor: () => <div>The base layer is configured by the server admin.</div>,
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
builder.addNestedOptions(
|
|
||||||
getLayerEditor({
|
|
||||||
category: ['Base layer'],
|
|
||||||
path: 'basemap', // only one for now
|
|
||||||
basemaps: true,
|
|
||||||
current: context.options?.layers?.[0],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let layerCount = context.options?.layers?.length;
|
const selected = state.layers[state.selected];
|
||||||
if (layerCount == null || layerCount < 1) {
|
if (state.selected && selected) {
|
||||||
layerCount = 1;
|
builder.addNestedOptions(
|
||||||
}
|
getLayerEditor({
|
||||||
|
state: selected,
|
||||||
for (let i = 0; i < layerCount; i++) {
|
category: ['Data layer'],
|
||||||
let name = 'Data layer';
|
basemaps: false,
|
||||||
if (i > 0) {
|
})
|
||||||
name += ` (${i + 1})`;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselayer = state.layers[0];
|
||||||
|
if (config.geomapDisableCustomBaseLayer) {
|
||||||
|
builder.addCustomEditor({
|
||||||
|
category: ['Base layer'],
|
||||||
|
id: 'layers',
|
||||||
|
path: '',
|
||||||
|
name: '',
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
editor: () => <div>The base layer is configured by the server admin.</div>,
|
||||||
|
});
|
||||||
|
} else if (baselayer) {
|
||||||
|
builder.addNestedOptions(
|
||||||
|
getLayerEditor({
|
||||||
|
state: baselayer,
|
||||||
|
category: ['Base layer'],
|
||||||
|
basemaps: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
builder.addNestedOptions(
|
|
||||||
getLayerEditor({
|
|
||||||
category: [name],
|
|
||||||
path: `layers[${i}]`, // only one for now
|
|
||||||
basemaps: false,
|
|
||||||
current: context.options?.layers?.[i],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The controls section
|
// The controls section
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MapLayerOptions } from '@grafana/data';
|
import { MapLayerHandler, MapLayerOptions } from '@grafana/data';
|
||||||
|
import BaseLayer from 'ol/layer/Base';
|
||||||
import { Units } from 'ol/proj/Units';
|
import { Units } from 'ol/proj/Units';
|
||||||
import { Style } from 'ol/style';
|
import { Style } from 'ol/style';
|
||||||
import { MapCenterID } from './view';
|
import { MapCenterID } from './view';
|
||||||
@ -62,6 +63,18 @@ export enum ComparisonOperation {
|
|||||||
GT = 'gt',
|
GT = 'gt',
|
||||||
GTE = 'gte',
|
GTE = 'gte',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-------------------
|
||||||
|
// Runtime model
|
||||||
|
//-------------------
|
||||||
|
export interface MapLayerState<TConfig = any> {
|
||||||
|
UID: string; // value changes with each initalization
|
||||||
|
options: MapLayerOptions<TConfig>;
|
||||||
|
handler: MapLayerHandler;
|
||||||
|
layer: BaseLayer; // the openlayers instance
|
||||||
|
onChange: (cfg: MapLayerOptions<TConfig>) => void;
|
||||||
|
isBasemap?: boolean;
|
||||||
|
}
|
||||||
export interface StyleMakerConfig {
|
export interface StyleMakerConfig {
|
||||||
color: string;
|
color: string;
|
||||||
fillColor: string;
|
fillColor: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user