mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: use a common configuration builder to find location fields (#36768)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
@@ -31,7 +31,7 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<CartoConfig>, theme: GrafanaTheme2) => ({
|
||||
create: (map: Map, options: MapLayerOptions<CartoConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...defaultCartoConfig, ...options.config };
|
||||
let style = cfg.theme as string;
|
||||
@@ -55,7 +55,7 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'theme',
|
||||
path: 'config.theme',
|
||||
name: 'Theme',
|
||||
settings: {
|
||||
options: [
|
||||
@@ -67,7 +67,7 @@ export const carto: MapLayerRegistryItem<CartoConfig> = {
|
||||
defaultValue: defaultCartoConfig.theme!,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
path: 'config.showLabels',
|
||||
name: 'Show labels',
|
||||
description: '',
|
||||
defaultValue: defaultCartoConfig.showLabels,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2, RegistryItem, Registry } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2, RegistryItem, Registry } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import { xyzTiles, defaultXYZConfig, XYZConfig } from './generic';
|
||||
|
||||
@@ -57,7 +57,7 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
||||
name: 'ArcGIS MapServer',
|
||||
isBaseMap: true,
|
||||
|
||||
create: (map: Map, options: MapLayerConfig<ESRIXYZConfig>, theme: GrafanaTheme2) => ({
|
||||
create: (map: Map, options: MapLayerOptions<ESRIXYZConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...options.config };
|
||||
const svc = publicServiceRegistry.getIfExists(cfg.server ?? DEFAULT_SERVICE)!;
|
||||
@@ -74,14 +74,14 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'server',
|
||||
path: 'config.server',
|
||||
name: 'Server instance',
|
||||
settings: {
|
||||
options: publicServiceRegistry.selectOptions().options,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'url',
|
||||
path: 'config.url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
@@ -90,7 +90,7 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
||||
showIf: (cfg) => cfg.server === CUSTOM_SERVICE,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'attribution',
|
||||
path: 'config.attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
@@ -21,7 +21,7 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
||||
name: 'XYZ Tile layer',
|
||||
isBaseMap: true,
|
||||
|
||||
create: (map: Map, options: MapLayerConfig<XYZConfig>, theme: GrafanaTheme2) => ({
|
||||
create: (map: Map, options: MapLayerOptions<XYZConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...options.config };
|
||||
if (!cfg.url) {
|
||||
@@ -42,7 +42,7 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addTextInput({
|
||||
path: 'url',
|
||||
path: 'config.url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
@@ -50,7 +50,7 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'attribution',
|
||||
path: 'config.attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import OSM from 'ol/source/OSM';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
@@ -12,7 +12,7 @@ const standard: MapLayerRegistryItem = {
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig) => ({
|
||||
create: (map: Map, options: MapLayerOptions) => ({
|
||||
init: () => {
|
||||
return new TileLayer({
|
||||
source: new OSM(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
@@ -29,7 +29,7 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<GeoJSONMapperConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
create: (map: Map, options: MapLayerOptions<GeoJSONMapperConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
|
||||
const source = new VectorSource({
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, Field, GrafanaTheme2 } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import Feature from 'ol/Feature';
|
||||
import * as style from 'ol/style';
|
||||
import * as source from 'ol/source';
|
||||
import * as layer from 'ol/layer';
|
||||
import Point from 'ol/geom/Point';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
|
||||
export interface LastPointConfig {
|
||||
icon?: string;
|
||||
@@ -20,12 +19,13 @@ export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = {
|
||||
name: 'Icon at last point',
|
||||
description: 'Show an icon at the last point',
|
||||
isBaseMap: false,
|
||||
showLocation: true,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<LastPointConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
create: (map: Map, options: MapLayerOptions<LastPointConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const point = new Feature({});
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
|
||||
@@ -45,28 +45,21 @@ export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = {
|
||||
source: vectorSource,
|
||||
});
|
||||
|
||||
const matchers = getLocationMatchers(options.location);
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
const frame = data.series[0];
|
||||
if (frame && frame.length) {
|
||||
let lat: Field | undefined = undefined;
|
||||
let lng: Field | undefined = undefined;
|
||||
for (const field of frame.fields) {
|
||||
if (field.name === 'lat') {
|
||||
lat = field;
|
||||
} else if (field.name === 'lng') {
|
||||
lng = field;
|
||||
}
|
||||
const info = dataFrameToPoints(frame, matchers);
|
||||
if(info.warning) {
|
||||
console.log( 'WARN', info.warning);
|
||||
return; // ???
|
||||
}
|
||||
|
||||
if (lat && lng) {
|
||||
const idx = lat.values.length - 1;
|
||||
const latV = lat.values.get(idx);
|
||||
const lngV = lng.values.get(idx);
|
||||
if (latV != null && lngV != null) {
|
||||
point.setGeometry(new Point(fromLonLat([lngV, latV])));
|
||||
}
|
||||
if(info.points?.length) {
|
||||
const last = info.points[info.points.length-1];
|
||||
point.setGeometry(last);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
import React from 'react';
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs, FieldType } from '@grafana/data';
|
||||
import { dataFrameToPoints } from './utils'
|
||||
import { FieldMappingOptions, QueryFormat } from '../../types'
|
||||
import { MapLayerRegistryItem, MapLayerOptions, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs, FieldType } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import Feature from 'ol/Feature';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface MarkersConfig {
|
||||
queryFormat: QueryFormat,
|
||||
fieldMapping: FieldMappingOptions,
|
||||
minSize: number,
|
||||
maxSize: number,
|
||||
opacity: number,
|
||||
}
|
||||
|
||||
const defaultOptions: MarkersConfig = {
|
||||
queryFormat: {
|
||||
locationType: 'coordinates',
|
||||
},
|
||||
fieldMapping: {
|
||||
metricField: '',
|
||||
geohashField: '',
|
||||
latitudeField: '',
|
||||
longitudeField: '',
|
||||
},
|
||||
minSize: 1,
|
||||
maxSize: 10,
|
||||
opacity: 0.4,
|
||||
@@ -43,24 +30,32 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
name: 'Markers',
|
||||
description: 'use markers to render each data point',
|
||||
isBaseMap: false,
|
||||
showLocation: true,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<MarkersConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
create: (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
const matchers = getLocationMatchers(options.location);
|
||||
|
||||
const vectorLayer = new layer.Vector({});
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
const features: Feature[] = [];
|
||||
const frame = data.series[0];
|
||||
if(!data.series?.length) {
|
||||
return; // ignore empty
|
||||
}
|
||||
|
||||
// Get data values
|
||||
const points = dataFrameToPoints(frame, config.fieldMapping, config.queryFormat);
|
||||
const field = frame.fields.find(field => field.name === config.fieldMapping.metricField);
|
||||
const frame = data.series[0];
|
||||
const info = dataFrameToPoints(frame, matchers);
|
||||
if(info.warning) {
|
||||
console.log( 'WARN', info.warning);
|
||||
return; // ???
|
||||
}
|
||||
|
||||
const field = frame.fields.find(field => field.type === FieldType.number); // TODO!!!!
|
||||
// Return early if metric field is not matched
|
||||
if (field === undefined) {
|
||||
return;
|
||||
@@ -76,6 +71,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
]
|
||||
});
|
||||
|
||||
const features: Feature[] = [];
|
||||
|
||||
// Map each data value into new points
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
// Get the circle color for a specific data value depending on color scheme
|
||||
@@ -88,7 +85,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
|
||||
// Create a new Feature for each point returned from dataFrameToPoints
|
||||
const dot = new Feature({
|
||||
geometry: points[i],
|
||||
geometry: info.points[i],
|
||||
});
|
||||
|
||||
// Set the style of each feature dot
|
||||
@@ -117,97 +114,29 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
// Circle overlay options
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'queryFormat.locationType',
|
||||
name: 'Location source',
|
||||
defaultValue: defaultOptions.queryFormat.locationType,
|
||||
settings: {
|
||||
options: [
|
||||
{
|
||||
value: 'coordinates',
|
||||
label: 'Latitude/Longitude fields',
|
||||
},
|
||||
{
|
||||
value: 'geohash',
|
||||
label: 'Geohash field',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'fieldMapping.latitudeField',
|
||||
name: 'Latitude Field',
|
||||
defaultValue: defaultOptions.fieldMapping.latitudeField,
|
||||
settings: {
|
||||
filter: (f) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (config) =>
|
||||
config.queryFormat.locationType === 'coordinates',
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'fieldMapping.longitudeField',
|
||||
name: 'Longitude Field',
|
||||
defaultValue: defaultOptions.fieldMapping.longitudeField,
|
||||
settings: {
|
||||
filter: (f) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
showIf: (config) =>
|
||||
config.queryFormat.locationType === 'coordinates',
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'fieldMapping.geohashField',
|
||||
name: 'Geohash Field',
|
||||
defaultValue: defaultOptions.fieldMapping.geohashField,
|
||||
settings: {
|
||||
filter: (f) => f.type === FieldType.string,
|
||||
noFieldsMessage: 'No strings fields found',
|
||||
info: ({
|
||||
name,
|
||||
field,
|
||||
}) => {
|
||||
if(!name || !field) {
|
||||
return <div>Select a field that contains <a href="https://en.wikipedia.org/wiki/Geohash">geohash</a> values in each row.</div>
|
||||
}
|
||||
const first = reduceField({field, reducers:[ReducerID.firstNotNull]})[ReducerID.firstNotNull] as string;
|
||||
if(!first) {
|
||||
return <div>No values found</div>
|
||||
}
|
||||
// const coords = decodeGeohash(first);
|
||||
// if(coords) {
|
||||
// return <div>First value: {`${coords}`} // {new Date().toISOString()}</div>
|
||||
// }
|
||||
// return <div>Invalid first value: {`${first}`}</div>;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
showIf: (config) =>
|
||||
config.queryFormat.locationType === 'geohash',
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'fieldMapping.metricField',
|
||||
name: 'Metric Field',
|
||||
defaultValue: defaultOptions.fieldMapping.metricField,
|
||||
settings: {
|
||||
filter: (f) => f.type === FieldType.number,
|
||||
noFieldsMessage: 'No numeric fields found',
|
||||
},
|
||||
})
|
||||
// .addFieldNamePicker({
|
||||
// path: 'fieldMapping.metricField',
|
||||
// name: 'Metric Field',
|
||||
// defaultValue: defaultOptions.fieldMapping.metricField,
|
||||
// settings: {
|
||||
// filter: (f) => f.type === FieldType.number,
|
||||
// noFieldsMessage: 'No numeric fields found',
|
||||
// },
|
||||
// })
|
||||
.addNumberInput({
|
||||
path: 'minSize',
|
||||
path: 'config.minSize',
|
||||
description: 'configures the min circle size',
|
||||
name: 'Min Size',
|
||||
defaultValue: defaultOptions.minSize,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'maxSize',
|
||||
path: 'config.maxSize',
|
||||
description: 'configures the max circle size',
|
||||
name: 'Max Size',
|
||||
defaultValue: defaultOptions.maxSize,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'opacity',
|
||||
path: 'config.opacity',
|
||||
description: 'configures the amount of transparency',
|
||||
name: 'Opacity',
|
||||
defaultValue: defaultOptions.opacity,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { DataFrame, DataFrameView } from '@grafana/data';
|
||||
import { Point } from 'ol/geom';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { FieldMappingOptions, QueryFormat } from '../../types'
|
||||
|
||||
/**
|
||||
* Function that formats dataframe into a Point[]
|
||||
*/
|
||||
export function dataFrameToPoints(frame: DataFrame, fieldMapping: FieldMappingOptions, queryFormat: QueryFormat): Point[] {
|
||||
|
||||
let points: Point[] = [];
|
||||
|
||||
if (frame && frame.length > 0) {
|
||||
|
||||
// For each data point, create a Point
|
||||
const view = new DataFrameView(frame);
|
||||
view.forEach(row => {
|
||||
|
||||
let lng;
|
||||
let lat;
|
||||
|
||||
// Coordinate Data
|
||||
if (queryFormat.locationType === "coordinates") {
|
||||
lng = row[fieldMapping.longitudeField];
|
||||
lat = row[fieldMapping.latitudeField];
|
||||
}
|
||||
// Geohash Data
|
||||
else if (queryFormat.locationType === "geohash"){
|
||||
const encodedGeohash = row[fieldMapping.geohashField];
|
||||
const decodedGeohash = decodeGeohash(encodedGeohash);
|
||||
lng = decodedGeohash.longitude;
|
||||
lat = decodedGeohash.latitude;
|
||||
}
|
||||
points.push(new Point(fromLonLat([lng,lat])));
|
||||
});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that decodes input geohash into latitude and longitude
|
||||
*/
|
||||
function decodeGeohash(geohash: string) {
|
||||
if (!geohash || geohash.length === 0) {
|
||||
throw new Error('Missing geohash value');
|
||||
}
|
||||
const BITS = [16, 8, 4, 2, 1];
|
||||
const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
|
||||
let isEven = true;
|
||||
const lat: number[] = [];
|
||||
const lon: number[] = [];
|
||||
lat[0] = -90.0;
|
||||
lat[1] = 90.0;
|
||||
lon[0] = -180.0;
|
||||
lon[1] = 180.0;
|
||||
let base32Decoded: number;
|
||||
|
||||
geohash.split('').forEach((item: string) => {
|
||||
base32Decoded = BASE32.indexOf(item);
|
||||
BITS.forEach(mask => {
|
||||
if (isEven) {
|
||||
refineInterval(lon, base32Decoded, mask);
|
||||
}
|
||||
else {
|
||||
refineInterval(lat, base32Decoded, mask);
|
||||
}
|
||||
isEven = !isEven;
|
||||
});
|
||||
});
|
||||
const latCenter = (lat[0] + lat[1]) / 2;
|
||||
const lonCenter = (lon[0] + lon[1]) / 2;
|
||||
|
||||
return { latitude: latCenter, longitude: lonCenter };
|
||||
}
|
||||
|
||||
function refineInterval(interval: any[], base32Decoded: number, mask: number) {
|
||||
/* tslint:disable no-bitwise*/
|
||||
if (base32Decoded & mask) {
|
||||
interval[0] = (interval[0] + interval[1]) / 2;
|
||||
}
|
||||
else {
|
||||
interval[1] = (interval[0] + interval[1]) / 2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user