mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Route/path visualization (#43554)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
0c0cf36ab8
commit
0e1f0dd8f5
@ -1,8 +1,8 @@
|
|||||||
import { Feature } from 'ol';
|
import { Feature } from 'ol';
|
||||||
import { Geometry } from 'ol/geom';
|
import { Geometry, LineString, Point } from 'ol/geom';
|
||||||
import VectorSource from 'ol/source/Vector';
|
import VectorSource from 'ol/source/Vector';
|
||||||
|
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame, Field } from '@grafana/data';
|
||||||
|
|
||||||
import { getGeometryField, LocationFieldMatchers } from './location';
|
import { getGeometryField, LocationFieldMatchers } from './location';
|
||||||
|
|
||||||
@ -34,4 +34,26 @@ export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSour
|
|||||||
// only call this at the end
|
// only call this at the end
|
||||||
this.changed();
|
this.changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLineString(frame: DataFrame) {
|
||||||
|
this.clear(true);
|
||||||
|
const info = getGeometryField(frame, this.location);
|
||||||
|
if (!info.field) {
|
||||||
|
this.changed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = info.field as Field<Point>;
|
||||||
|
const geometry = new LineString(field.values.toArray().map((p) => p.getCoordinates())) as Geometry;
|
||||||
|
this.addFeatureInternal(
|
||||||
|
new Feature({
|
||||||
|
frame,
|
||||||
|
rowIndex: 0,
|
||||||
|
geometry: geometry as T,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// only call this at the end
|
||||||
|
this.changed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ import { markersLayer } from './markersLayer';
|
|||||||
import { geojsonLayer } from './geojsonLayer';
|
import { geojsonLayer } from './geojsonLayer';
|
||||||
import { heatmapLayer } from './heatMap';
|
import { heatmapLayer } from './heatMap';
|
||||||
import { lastPointTracker } from './lastPointTracker';
|
import { lastPointTracker } from './lastPointTracker';
|
||||||
|
import { routeLayer } from './routeLayer';
|
||||||
import { dayNightLayer } from './dayNightLayer';
|
import { dayNightLayer } from './dayNightLayer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry for layer handlers
|
* Registry for layer handlers
|
||||||
*/
|
*/
|
||||||
export const dataLayers = [markersLayer, heatmapLayer, lastPointTracker, geojsonLayer, dayNightLayer];
|
export const dataLayers = [markersLayer, heatmapLayer, lastPointTracker, geojsonLayer, dayNightLayer, routeLayer];
|
||||||
|
147
public/app/plugins/panel/geomap/layers/data/routeLayer.tsx
Normal file
147
public/app/plugins/panel/geomap/layers/data/routeLayer.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
MapLayerRegistryItem,
|
||||||
|
MapLayerOptions,
|
||||||
|
PanelData,
|
||||||
|
GrafanaTheme2,
|
||||||
|
FrameGeometrySourceMode,
|
||||||
|
PluginState,
|
||||||
|
EventBus,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import Map from 'ol/Map';
|
||||||
|
import { FeatureLike } from 'ol/Feature';
|
||||||
|
import { getLocationMatchers } from 'app/features/geo/utils/location';
|
||||||
|
import { getColorDimension } from 'app/features/dimensions';
|
||||||
|
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
||||||
|
import { StyleEditor } from './StyleEditor';
|
||||||
|
import { getStyleConfigState } from '../../style/utils';
|
||||||
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
import { routeStyle } from '../../style/markers';
|
||||||
|
import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource';
|
||||||
|
|
||||||
|
// Configuration options for Circle overlays
|
||||||
|
export interface RouteConfig {
|
||||||
|
style: StyleConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: RouteConfig = {
|
||||||
|
style: {
|
||||||
|
...defaultStyleConfig,
|
||||||
|
opacity: 1,
|
||||||
|
lineWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ROUTE_LAYER_ID = 'route';
|
||||||
|
|
||||||
|
// Used by default when nothing is configured
|
||||||
|
export const defaultRouteConfig: MapLayerOptions<RouteConfig> = {
|
||||||
|
type: ROUTE_LAYER_ID,
|
||||||
|
name: '', // will get replaced
|
||||||
|
config: defaultOptions,
|
||||||
|
location: {
|
||||||
|
mode: FrameGeometrySourceMode.Auto,
|
||||||
|
},
|
||||||
|
tooltip: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map layer configuration for circle overlay
|
||||||
|
*/
|
||||||
|
export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
||||||
|
id: ROUTE_LAYER_ID,
|
||||||
|
name: 'Route',
|
||||||
|
description: 'Render data points as a route',
|
||||||
|
isBaseMap: false,
|
||||||
|
showLocation: true,
|
||||||
|
state: PluginState.alpha,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that configures transformation and returns a transformer
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
create: async (map: Map, options: MapLayerOptions<RouteConfig>, eventBus: EventBus, theme: GrafanaTheme2) => {
|
||||||
|
// Assert default values
|
||||||
|
const config = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options?.config,
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = await getStyleConfigState(config.style);
|
||||||
|
const location = await getLocationMatchers(options.location);
|
||||||
|
const source = new FrameVectorSource(location);
|
||||||
|
const vectorLayer = new VectorLayer({
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!style.fields) {
|
||||||
|
// Set a global style
|
||||||
|
vectorLayer.setStyle(routeStyle(style.base));
|
||||||
|
} else {
|
||||||
|
vectorLayer.setStyle((feature: FeatureLike) => {
|
||||||
|
const idx = feature.get('rowIndex') as number;
|
||||||
|
const dims = style.dims;
|
||||||
|
if (!dims || !isNumber(idx)) {
|
||||||
|
return routeStyle(style.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = { ...style.base };
|
||||||
|
|
||||||
|
if (dims.color) {
|
||||||
|
values.color = dims.color.get(idx);
|
||||||
|
}
|
||||||
|
return routeStyle(values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: () => vectorLayer,
|
||||||
|
update: (data: PanelData) => {
|
||||||
|
if (!data.series?.length) {
|
||||||
|
return; // ignore empty
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const frame of data.series) {
|
||||||
|
if (style.fields) {
|
||||||
|
const dims: StyleDimensions = {};
|
||||||
|
if (style.fields.color) {
|
||||||
|
dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme);
|
||||||
|
}
|
||||||
|
style.dims = dims;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.updateLineString(frame);
|
||||||
|
break; // Only the first frame for now!
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Route layer options
|
||||||
|
registerOptionsUI: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'config.style',
|
||||||
|
path: 'config.style',
|
||||||
|
name: 'Style',
|
||||||
|
editor: StyleEditor,
|
||||||
|
settings: {
|
||||||
|
simpleFixedValues: true,
|
||||||
|
},
|
||||||
|
defaultValue: defaultOptions.style,
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'config.style.lineWidth',
|
||||||
|
name: 'Line width',
|
||||||
|
defaultValue: defaultOptions.style.lineWidth,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// fill in the default values
|
||||||
|
defaultOptions,
|
||||||
|
};
|
@ -42,6 +42,18 @@ export function getFillColor(cfg: StyleConfigValues) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStrokeStyle(cfg: StyleConfigValues) {
|
||||||
|
const opacity = cfg.opacity == null ? 0.8 : cfg.opacity;
|
||||||
|
if (opacity === 1) {
|
||||||
|
return new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 });
|
||||||
|
}
|
||||||
|
if (opacity > 0) {
|
||||||
|
const color = tinycolor(cfg.color).setAlpha(opacity).toRgbString();
|
||||||
|
return new Stroke({ color, width: cfg.lineWidth ?? 1 });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const textLabel = (cfg: StyleConfigValues) => {
|
const textLabel = (cfg: StyleConfigValues) => {
|
||||||
if (!cfg.text) {
|
if (!cfg.text) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -88,6 +100,14 @@ export const polyStyle = (cfg: StyleConfigValues) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const routeStyle = (cfg: StyleConfigValues) => {
|
||||||
|
return new Style({
|
||||||
|
fill: getFillColor(cfg),
|
||||||
|
stroke: getStrokeStyle(cfg),
|
||||||
|
text: textLabel(cfg),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Square and cross
|
// Square and cross
|
||||||
const errorMarker = (cfg: StyleConfigValues) => {
|
const errorMarker = (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
Loading…
Reference in New Issue
Block a user