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 { Geometry } from 'ol/geom';
|
||||
import { Geometry, LineString, Point } from 'ol/geom';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
|
||||
import { getGeometryField, LocationFieldMatchers } from './location';
|
||||
|
||||
@ -34,4 +34,26 @@ export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSour
|
||||
// only call this at the end
|
||||
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 { heatmapLayer } from './heatMap';
|
||||
import { lastPointTracker } from './lastPointTracker';
|
||||
import { routeLayer } from './routeLayer';
|
||||
import { dayNightLayer } from './dayNightLayer';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!cfg.text) {
|
||||
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
|
||||
const errorMarker = (cfg: StyleConfigValues) => {
|
||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||
|
Loading…
Reference in New Issue
Block a user