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
|
||||
*/
|
||||
export interface MapLayerHandler {
|
||||
export interface MapLayerHandler<TConfig = any> {
|
||||
init: () => BaseLayer;
|
||||
update?: (data: PanelData) => void;
|
||||
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
|
||||
*/
|
||||
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,
|
||||
unitOverrideProcessor,
|
||||
FieldNamePickerConfigSettings,
|
||||
StandardEditorContext,
|
||||
} from '../field';
|
||||
import { PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||
|
||||
@ -133,6 +134,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
|
||||
export interface NestedValueAccess {
|
||||
getValue: (path: string) => any;
|
||||
onChange: (path: string, value: any) => void;
|
||||
getContext?: (parent: StandardEditorContext<any, any>) => StandardEditorContext<any, any>;
|
||||
}
|
||||
export interface NestedPanelOptions<TSub = any> {
|
||||
path: string;
|
||||
|
@ -136,12 +136,16 @@ export function fillOptionsPaneItems(
|
||||
|
||||
// Nested options get passed up one level
|
||||
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(
|
||||
pluginOption.getBuilder(),
|
||||
pluginOption.getNestedValueAccess(access),
|
||||
subAccess,
|
||||
getOptionsPaneCategory,
|
||||
{ ...context, options: sub },
|
||||
subContext,
|
||||
category // parent category
|
||||
);
|
||||
continue;
|
||||
|
@ -14,7 +14,7 @@ import appEvents from 'app/core/app_events';
|
||||
type Props = StandardEditorProps<any, InstanceState, PanelOptions>;
|
||||
|
||||
export class LayerElementListEditor extends PureComponent<Props> {
|
||||
style = getStyles(config.theme);
|
||||
style = getLayerDragStyles(config.theme);
|
||||
|
||||
onAddItem = (sel: SelectableValue<string>) => {
|
||||
// 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`
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
`,
|
||||
|
@ -1,16 +1,14 @@
|
||||
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 Attribution from 'ol/control/Attribution';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
|
||||
import {
|
||||
PanelData,
|
||||
MapLayerHandler,
|
||||
MapLayerOptions,
|
||||
PanelProps,
|
||||
GrafanaTheme,
|
||||
@ -20,7 +18,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig } from './types';
|
||||
import { centerPointRegistry, MapCenterID } from './view';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
@ -32,12 +30,10 @@ import { getGlobalStyles } from './globalStyles';
|
||||
import { Global } from '@emotion/react';
|
||||
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
|
||||
import { DataHoverView } from './components/DataHoverView';
|
||||
|
||||
interface MapLayerState {
|
||||
config: MapLayerOptions;
|
||||
handler: MapLayerHandler;
|
||||
layer: BaseLayer; // used to add|remove
|
||||
}
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PanelEditExitedEvent } from 'app/types/events';
|
||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
let sharedView: View | undefined = undefined;
|
||||
@ -47,31 +43,51 @@ interface State extends OverlayProps {
|
||||
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> {
|
||||
static contextType = PanelContextRoot;
|
||||
panelContext: PanelContext = {} as PanelContext;
|
||||
private subs = new Subscription();
|
||||
|
||||
globalCSS = getGlobalStyles(config.theme2);
|
||||
|
||||
counter = 0;
|
||||
map?: Map;
|
||||
basemap?: BaseLayer;
|
||||
layers: MapLayerState[] = [];
|
||||
mouseWheelZoom?: MouseWheelZoom;
|
||||
style = getStyles(config.theme);
|
||||
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
|
||||
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
|
||||
|
||||
map?: Map;
|
||||
mapDiv?: HTMLDivElement;
|
||||
layers: MapLayerState[] = [];
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||
if (this.mapDiv && this.props.id === evt.payload) {
|
||||
this.initMapRef(this.mapDiv);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.panelContext = this.context as PanelContext;
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
@ -84,25 +100,94 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.map.updateSize();
|
||||
}
|
||||
|
||||
// External configuration changed
|
||||
let layersChanged = false;
|
||||
if (this.props.options !== nextProps.options) {
|
||||
layersChanged = this.optionsChanged(nextProps.options);
|
||||
}
|
||||
|
||||
// External data changed
|
||||
if (layersChanged || this.props.data !== nextProps.data) {
|
||||
if (this.props.data !== nextProps.data) {
|
||||
this.dataChanged(nextProps.data);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* NOTE: changes to basemap and layers are handled independently
|
||||
*/
|
||||
optionsChanged(options: GeomapPanelOptions): boolean {
|
||||
let layersChanged = false;
|
||||
optionsChanged(options: GeomapPanelOptions) {
|
||||
const oldOptions = this.props.options;
|
||||
console.log('options changed!', options);
|
||||
|
||||
@ -115,19 +200,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
console.log('Controls changed');
|
||||
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
|
||||
}
|
||||
|
||||
if (options.basemap !== oldOptions.basemap) {
|
||||
console.log('Basemap changed');
|
||||
this.initBasemap(options.basemap);
|
||||
layersChanged = true;
|
||||
}
|
||||
|
||||
if (options.layers !== oldOptions.layers) {
|
||||
console.log('layers changed');
|
||||
this.initLayers(options.layers ?? []); // async
|
||||
layersChanged = true;
|
||||
}
|
||||
return layersChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,6 +214,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
initMapRef = async (div: HTMLDivElement) => {
|
||||
this.mapDiv = div;
|
||||
if (this.map) {
|
||||
this.map.dispose();
|
||||
}
|
||||
@ -151,7 +224,8 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
const { options } = this.props;
|
||||
this.map = new Map({
|
||||
|
||||
const map = (this.map = new Map({
|
||||
view: this.initMapView(options.view),
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
@ -160,12 +234,33 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
interactions: interactionDefaults({
|
||||
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.map.addInteraction(this.mouseWheelZoom);
|
||||
this.initControls(options.controls);
|
||||
this.initBasemap(options.basemap);
|
||||
await this.initLayers(options.layers);
|
||||
this.forceUpdate(); // first render
|
||||
|
||||
// Tooltip listener
|
||||
@ -173,6 +268,22 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||
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>) => {
|
||||
@ -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) {
|
||||
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) {
|
||||
cfg = DEFAULT_BASEMAP_CONFIG;
|
||||
// Use default makers layer
|
||||
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();
|
||||
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[]) {
|
||||
// 1st remove existing layers
|
||||
for (const state of this.layers) {
|
||||
this.map!.removeLayer(state.layer);
|
||||
state.layer.dispose();
|
||||
// const key = layer.on('change', () => {
|
||||
// const state = layer.getLayerState();
|
||||
// console.log('LAYER', key, state);
|
||||
// });
|
||||
|
||||
if (handler.update) {
|
||||
handler.update(this.props.data);
|
||||
}
|
||||
|
||||
if (!layers) {
|
||||
layers = [];
|
||||
}
|
||||
const UID = `lyr-${this.counter++}`;
|
||||
return {
|
||||
UID,
|
||||
isBasemap,
|
||||
options,
|
||||
layer,
|
||||
handler,
|
||||
|
||||
const legends: React.ReactNode[] = [];
|
||||
this.layers = [];
|
||||
for (const overlay of layers) {
|
||||
const item = geomapLayerRegistry.getIfExists(overlay.type);
|
||||
if (!item) {
|
||||
console.warn('unknown layer type: ', overlay);
|
||||
continue; // TODO -- panel warning?
|
||||
}
|
||||
|
||||
const handler = 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);
|
||||
// Used by the editors
|
||||
onChange: (cfg) => {
|
||||
this.updateLayer(UID, cfg);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
initMapView(config: MapViewConfig): View {
|
||||
@ -367,7 +508,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
|
||||
</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 { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||
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,
|
||||
onChange,
|
||||
context,
|
||||
|
@ -11,38 +11,51 @@ import { GazetteerPathEditor } from './GazetteerPathEditor';
|
||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { defaultMarkersConfig } from '../layers/data/markersLayer';
|
||||
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 {
|
||||
state: MapLayerState;
|
||||
category: string[];
|
||||
path: string;
|
||||
basemaps: boolean; // only basemaps
|
||||
current?: MapLayerOptions;
|
||||
}
|
||||
|
||||
export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<MapLayerOptions> {
|
||||
return {
|
||||
category: opts.category,
|
||||
path: opts.path,
|
||||
path: '--', // Not used
|
||||
defaultValue: opts.basemaps ? DEFAULT_BASEMAP_CONFIG : defaultMarkersConfig,
|
||||
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) => {
|
||||
const { state } = opts;
|
||||
const { options } = state;
|
||||
if (path === 'type' && value) {
|
||||
const layer = geomapLayerRegistry.getIfExists(value);
|
||||
if (layer) {
|
||||
parent.onChange(opts.path, {
|
||||
...opts.current, // keep current shared options
|
||||
console.log('Change layer type:', value, state);
|
||||
state.onChange({
|
||||
...options, // keep current shared options
|
||||
type: layer.id,
|
||||
config: { ...layer.defaultOptions }, // clone?
|
||||
});
|
||||
return; // reset current values
|
||||
return;
|
||||
}
|
||||
}
|
||||
parent.onChange(`${opts.path}.${path}`, value);
|
||||
state.onChange(setOptionImmutably(options, path, value));
|
||||
},
|
||||
}),
|
||||
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 layerTypes = geomapLayerRegistry.selectOptions(
|
||||
@ -54,81 +67,88 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
|
||||
|
||||
builder.addSelect({
|
||||
path: 'type',
|
||||
name: undefined as any, // required, but hide space
|
||||
name: 'Layer type', // required, but hide space
|
||||
settings: {
|
||||
options: layerTypes.options,
|
||||
},
|
||||
});
|
||||
|
||||
if (layer) {
|
||||
if (layer.showLocation) {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'location.mode',
|
||||
name: 'Location',
|
||||
description: '',
|
||||
defaultValue: FrameGeometrySourceMode.Auto,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
|
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
|
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
|
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.latitude',
|
||||
name: 'Latitude field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.longitude',
|
||||
name: 'Longitude field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.geohash',
|
||||
name: 'Geohash field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.string,
|
||||
noFieldsMessage: 'No strings fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash,
|
||||
// eslint-disable-next-line react/display-name
|
||||
// info: (props) => <div>HELLO</div>,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.lookup',
|
||||
name: 'Lookup field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.string,
|
||||
noFieldsMessage: 'No strings fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'gazetteer',
|
||||
path: 'location.gazetteer',
|
||||
name: 'Gazetteer',
|
||||
editor: GazetteerPathEditor,
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
||||
});
|
||||
}
|
||||
if (layer.registerOptionsUI) {
|
||||
layer.registerOptionsUI(builder);
|
||||
}
|
||||
if (layer.showOpacity) {
|
||||
// TODO -- add opacity check
|
||||
}
|
||||
if (!layer) {
|
||||
return; // unknown layer type
|
||||
}
|
||||
|
||||
// Don't show UI for default configuration
|
||||
if (options.type === DEFAULT_BASEMAP_CONFIG.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layer.showLocation) {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'location.mode',
|
||||
name: 'Location',
|
||||
description: '',
|
||||
defaultValue: FrameGeometrySourceMode.Auto,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
|
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
|
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
|
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.latitude',
|
||||
name: 'Latitude field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.longitude',
|
||||
name: 'Longitude field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.geohash',
|
||||
name: 'Geohash field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.string,
|
||||
noFieldsMessage: 'No strings fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash,
|
||||
// eslint-disable-next-line react/display-name
|
||||
// info: (props) => <div>HELLO</div>,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'location.lookup',
|
||||
name: 'Lookup field',
|
||||
settings: {
|
||||
filter: (f: Field) => f.type === FieldType.string,
|
||||
noFieldsMessage: 'No strings fields found',
|
||||
},
|
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'gazetteer',
|
||||
path: 'location.gazetteer',
|
||||
name: 'Gazetteer',
|
||||
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;
|
||||
}
|
||||
|
||||
function dataLayerFilter(layer: MapLayerRegistryItem): boolean {
|
||||
export function dataLayerFilter(layer: MapLayerRegistryItem): boolean {
|
||||
if (layer.isBaseMap) {
|
||||
return false;
|
||||
}
|
||||
|
@ -50,29 +50,29 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'config.theme',
|
||||
name: 'Theme',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
|
||||
{ value: LayerTheme.Light, label: 'Light' },
|
||||
{ value: LayerTheme.Dark, label: 'Dark' },
|
||||
],
|
||||
},
|
||||
defaultValue: defaultCartoConfig.theme!,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'config.showLabels',
|
||||
name: 'Show labels',
|
||||
description: '',
|
||||
defaultValue: defaultCartoConfig.showLabels,
|
||||
});
|
||||
},
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'config.theme',
|
||||
name: 'Theme',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
|
||||
{ value: LayerTheme.Light, label: 'Light' },
|
||||
{ value: LayerTheme.Dark, label: 'Dark' },
|
||||
],
|
||||
},
|
||||
defaultValue: defaultCartoConfig.theme!,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'config.showLabels',
|
||||
name: 'Show labels',
|
||||
description: '',
|
||||
defaultValue: defaultCartoConfig.showLabels,
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const cartoLayers = [carto];
|
||||
|
@ -66,35 +66,36 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
||||
cfg.attribution = `Tiles © <a href="${base}${svc.slug}/MapServer">ArcGIS</a>`;
|
||||
}
|
||||
const opts = { ...options, config: cfg as XYZConfig };
|
||||
return xyzTiles.create(map, opts, theme);
|
||||
},
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'config.server',
|
||||
name: 'Server instance',
|
||||
settings: {
|
||||
options: publicServiceRegistry.selectOptions().options,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'config.url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.url,
|
||||
},
|
||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'config.attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
},
|
||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||
});
|
||||
return xyzTiles.create(map, opts, theme).then((xyz) => {
|
||||
xyz.registerOptionsUI = (builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'config.server',
|
||||
name: 'Server instance',
|
||||
settings: {
|
||||
options: publicServiceRegistry.selectOptions().options,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'config.url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.url,
|
||||
},
|
||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'config.attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
},
|
||||
showIf: (cfg) => cfg.config?.server === CUSTOM_SERVICE,
|
||||
});
|
||||
};
|
||||
return xyz;
|
||||
});
|
||||
},
|
||||
|
||||
defaultOptions: {
|
||||
|
@ -37,26 +37,25 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
||||
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];
|
||||
|
@ -3,6 +3,7 @@ import Map from 'ol/Map';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
import { unByKey } from 'ol/Observable';
|
||||
import { Feature } from 'ol';
|
||||
import { Geometry } from 'ol/geom';
|
||||
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
|
||||
@ -53,6 +54,19 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
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({
|
||||
stroke: new Stroke({
|
||||
color: DEFAULT_STYLE_RULE.fillColor,
|
||||
@ -80,37 +94,40 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
update: (data: PanelData) => {
|
||||
console.log('todo... find values matching the ID and update');
|
||||
|
||||
// Update each feature
|
||||
source.getFeatures().forEach((f) => {
|
||||
console.log('Find: ', f.getId(), f.getProperties());
|
||||
});
|
||||
// // Update each feature
|
||||
// source.getFeatures().forEach((f) => {
|
||||
// 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,
|
||||
};
|
||||
|
@ -110,52 +110,53 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
||||
}
|
||||
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
|
||||
defaultOptions,
|
||||
};
|
||||
|
@ -165,67 +165,68 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
const vectorSource = new source.Vector({ features });
|
||||
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
|
||||
defaultOptions,
|
||||
|
@ -119,44 +119,44 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
const vectorSource = new source.Vector({ features });
|
||||
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,
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { GeomapPanel } from './GeomapPanel';
|
||||
import { GeomapInstanceState, GeomapPanel } from './GeomapPanel';
|
||||
import { MapViewEditor } from './editor/MapViewEditor';
|
||||
import { defaultView, GeomapPanelOptions } from './types';
|
||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { LayersEditor } from './editor/LayersEditor';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
@ -32,45 +33,48 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
defaultValue: defaultView.shared,
|
||||
});
|
||||
|
||||
// Check server settings to disable custom basemap settings
|
||||
if (config.geomapDisableCustomBaseLayer) {
|
||||
const state = context.instanceState as GeomapInstanceState;
|
||||
if (!state?.layers) {
|
||||
// TODO? show spinner?
|
||||
} else {
|
||||
builder.addCustomEditor({
|
||||
category: ['Base layer'],
|
||||
category: ['Data layer'],
|
||||
id: 'layers',
|
||||
path: '',
|
||||
name: '',
|
||||
// eslint-disable-next-line react/display-name
|
||||
editor: () => <div>The base layer is configured by the server admin.</div>,
|
||||
editor: LayersEditor,
|
||||
});
|
||||
} 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;
|
||||
if (layerCount == null || layerCount < 1) {
|
||||
layerCount = 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < layerCount; i++) {
|
||||
let name = 'Data layer';
|
||||
if (i > 0) {
|
||||
name += ` (${i + 1})`;
|
||||
const selected = state.layers[state.selected];
|
||||
if (state.selected && selected) {
|
||||
builder.addNestedOptions(
|
||||
getLayerEditor({
|
||||
state: selected,
|
||||
category: ['Data layer'],
|
||||
basemaps: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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 { Style } from 'ol/style';
|
||||
import { MapCenterID } from './view';
|
||||
@ -62,6 +63,18 @@ export enum ComparisonOperation {
|
||||
GT = 'gt',
|
||||
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 {
|
||||
color: string;
|
||||
fillColor: string;
|
||||
|
Loading…
Reference in New Issue
Block a user