1import React, { ReactNode } from 'react';
2import {
3  MapLayerRegistryItem,
4  MapLayerOptions,
5  PanelData,
6  GrafanaTheme2,
7  FrameGeometrySourceMode,
8} from '@grafana/data';
9import Map from 'ol/Map';
10import Feature from 'ol/Feature';
11import { Point } from 'ol/geom';
12import * as layer from 'ol/layer';
13import * as source from 'ol/source';
14import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
15import { getScaledDimension, getColorDimension, getTextDimension, getScalarDimension } from 'app/features/dimensions';
16import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
17import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
18import { ReplaySubject } from 'rxjs';
19import { getFeatures } from '../../utils/getFeatures';
20import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
21import { StyleEditor } from './StyleEditor';
22import { getStyleConfigState } from '../../style/utils';
23
24// Configuration options for Circle overlays
25export interface MarkersConfig {
26  style: StyleConfig;
27  showLegend?: boolean;
28}
29
30const defaultOptions: MarkersConfig = {
31  style: defaultStyleConfig,
32  showLegend: true,
33};
34
35export const MARKERS_LAYER_ID = 'markers';
36
37// Used by default when nothing is configured
38export const defaultMarkersConfig: MapLayerOptions<MarkersConfig> = {
39  type: MARKERS_LAYER_ID,
40  name: '', // will get replaced
41  config: defaultOptions,
42  location: {
43    mode: FrameGeometrySourceMode.Auto,
44  },
45};
46
47/**
48 * Map layer configuration for circle overlay
49 */
50export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
51  id: MARKERS_LAYER_ID,
52  name: 'Markers',
53  description: 'use markers to render each data point',
54  isBaseMap: false,
55  showLocation: true,
56
57  /**
58   * Function that configures transformation and returns a transformer
59   * @param options
60   */
61  create: async (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2) => {
62    const matchers = await getLocationMatchers(options.location);
63    const vectorLayer = new layer.Vector({});
64    // Assert default values
65    const config = {
66      ...defaultOptions,
67      ...options?.config,
68    };
69
70    const legendProps = new ReplaySubject<MarkersLegendProps>(1);
71    let legend: ReactNode = null;
72    if (config.showLegend) {
73      legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
74    }
75
76    // Set the default style
77    const style = await getStyleConfigState(config.style);
78    if (!style.fields) {
79      vectorLayer.setStyle(style.maker(style.base));
80    }
81
82    return {
83      init: () => vectorLayer,
84      legend: legend,
85      update: (data: PanelData) => {
86        if (!data.series?.length) {
87          return; // ignore empty
88        }
89
90        const features: Feature<Point>[] = [];
91
92        for (const frame of data.series) {
93          const info = dataFrameToPoints(frame, matchers);
94          if (info.warning) {
95            console.log('Could not find locations', info.warning);
96            continue; // ???
97          }
98
99          if (style.fields) {
100            const dims: StyleDimensions = {};
101            if (style.fields.color) {
102              dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme);
103            }
104            if (style.fields.size) {
105              dims.size = getScaledDimension(frame, style.config.size ?? defaultStyleConfig.size);
106            }
107            if (style.fields.text) {
108              dims.text = getTextDimension(frame, style.config.text!);
109            }
110            if (style.fields.rotation) {
111              dims.rotation = getScalarDimension(frame, style.config.rotation ?? defaultStyleConfig.rotation);
112            }
113            style.dims = dims;
114          }
115
116          const frameFeatures = getFeatures(frame, info, style);
117
118          if (frameFeatures) {
119            features.push(...frameFeatures);
120          }
121
122          // Post updates to the legend component
123          if (legend) {
124            legendProps.next({
125              color: style.dims?.color,
126              size: style.dims?.size,
127            });
128          }
129          break; // Only the first frame for now!
130        }
131
132        // Source reads the data and provides a set of features to visualize
133        const vectorSource = new source.Vector({ features });
134        vectorLayer.setSource(vectorSource);
135      },
136
137      // Marker overlay options
138      registerOptionsUI: (builder) => {
139        builder
140          .addCustomEditor({
141            id: 'config.style',
142            path: 'config.style',
143            name: 'Styles',
144            editor: StyleEditor,
145            settings: {
146              displayRotation: true,
147            },
148            defaultValue: defaultOptions.style,
149          })
150          .addBooleanSwitch({
151            path: 'config.showLegend',
152            name: 'Show legend',
153            description: 'Show legend',
154            defaultValue: defaultOptions.showLegend,
155          });
156      },
157    };
158  },
159
160  // fill in the default values
161  defaultOptions,
162};
163