mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Support shared crosshair for route layer (#51495)
This commit is contained in:
parent
ffd9d9d0c5
commit
21591be469
@ -8978,9 +8978,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
|
@ -7,11 +7,10 @@ import Attribution from 'ol/control/Attribution';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { createEmpty, extend, isEmpty } from 'ol/extent';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
@ -40,6 +39,7 @@ import { getGlobalStyles } from './globalStyles';
|
||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
|
||||
import { getLayersExtent } from './utils/getLayersExtent';
|
||||
import { centerPointRegistry, MapCenterID } from './view';
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
@ -590,11 +590,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||
} else if (v.id === MapCenterID.Fit) {
|
||||
var extent = layers
|
||||
.getArray()
|
||||
.filter((l) => l instanceof VectorLayer)
|
||||
.map((l) => (l as VectorLayer<any>).getSource().getExtent() ?? [])
|
||||
.reduce(extend, createEmpty());
|
||||
const extent = getLayersExtent(layers);
|
||||
if (!isEmpty(extent)) {
|
||||
view.fit(extent, {
|
||||
padding: [30, 30, 30, 30],
|
||||
|
@ -6,10 +6,15 @@ import {
|
||||
FrameGeometrySourceMode,
|
||||
PluginState,
|
||||
EventBus,
|
||||
DataHoverEvent,
|
||||
DataHoverClearEvent,
|
||||
DataFrame,
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
} from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { getLocationMatchers } from 'app/features/geo/utils/location';
|
||||
import { Subscription, throttleTime } from 'rxjs';
|
||||
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
|
||||
import { getColorDimension } from 'app/features/dimensions';
|
||||
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
||||
import { StyleEditor } from './StyleEditor';
|
||||
@ -18,6 +23,11 @@ import VectorLayer from 'ol/layer/Vector';
|
||||
import { isNumber } from 'lodash';
|
||||
import { routeStyle } from '../../style/markers';
|
||||
import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource';
|
||||
import { Group as LayerGroup } from 'ol/layer';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import { Fill, Stroke, Style, Circle } from 'ol/style';
|
||||
import Feature from 'ol/Feature';
|
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface RouteConfig {
|
||||
@ -70,9 +80,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
||||
const style = await getStyleConfigState(config.style);
|
||||
const location = await getLocationMatchers(options.location);
|
||||
const source = new FrameVectorSource(location);
|
||||
const vectorLayer = new VectorLayer({
|
||||
source,
|
||||
});
|
||||
const vectorLayer = new VectorLayer({ source });
|
||||
|
||||
if (!style.fields) {
|
||||
// Set a global style
|
||||
@ -94,8 +102,70 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
||||
});
|
||||
}
|
||||
|
||||
// Crosshair layer
|
||||
const crosshairFeature = new Feature({});
|
||||
const crosshairRadius = (style.base.lineWidth || 6) + 2;
|
||||
const crosshairStyle = new Style({
|
||||
image: new Circle({
|
||||
radius: crosshairRadius,
|
||||
stroke: new Stroke({
|
||||
color: alpha(style.base.color, 0.4),
|
||||
width: crosshairRadius + 2
|
||||
}),
|
||||
fill: new Fill({color: style.base.color}),
|
||||
})
|
||||
});
|
||||
|
||||
const crosshairLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [crosshairFeature],
|
||||
}),
|
||||
style: crosshairStyle,
|
||||
});
|
||||
|
||||
const layer = new LayerGroup({
|
||||
layers: [vectorLayer, crosshairLayer]
|
||||
});
|
||||
|
||||
// Crosshair sharing subscriptions
|
||||
const subscriptions = new Subscription();
|
||||
|
||||
subscriptions.add(
|
||||
eventBus
|
||||
.getStream(DataHoverEvent)
|
||||
.pipe(throttleTime(8))
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
const feature = source.getFeatures()[0];
|
||||
const frame = feature?.get('frame') as DataFrame;
|
||||
const time = event.payload?.point?.time as number;
|
||||
if (frame && time) {
|
||||
const timeField = frame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
if (timeField) {
|
||||
const timestamps: number[] = timeField.values.toArray();
|
||||
const pointIdx = findNearestTimeIndex(timestamps, time);
|
||||
if (pointIdx !== null) {
|
||||
const out = getGeometryField(frame, location);
|
||||
if (out.field) {
|
||||
crosshairFeature.setGeometry(out.field.values.get(pointIdx));
|
||||
crosshairFeature.setStyle(crosshairStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.add(
|
||||
eventBus.subscribe(DataHoverClearEvent, (event) => {
|
||||
crosshairFeature.setStyle(new Style({}));
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
init: () => layer,
|
||||
dispose: () => subscriptions.unsubscribe(),
|
||||
update: (data: PanelData) => {
|
||||
if (!data.series?.length) {
|
||||
return; // ignore empty
|
||||
@ -145,3 +215,34 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
||||
|
||||
function findNearestTimeIndex(timestamps: number[], time: number): number | null {
|
||||
if (timestamps.length === 0) {
|
||||
return null;
|
||||
} else if (timestamps.length === 1) {
|
||||
return 0;
|
||||
}
|
||||
const lastIdx = timestamps.length - 1;
|
||||
if (time < timestamps[0]) {
|
||||
return 0;
|
||||
} else if (time > timestamps[lastIdx]) {
|
||||
return lastIdx;
|
||||
}
|
||||
|
||||
const probableIdx = Math.abs(Math.round(lastIdx * (time - timestamps[0]) / (timestamps[lastIdx] - timestamps[0])));
|
||||
if (time < timestamps[probableIdx]) {
|
||||
for (let i = probableIdx; i > 0; i--) {
|
||||
if (time > timestamps[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
for (let i = probableIdx; i < lastIdx; i++) {
|
||||
if (time < timestamps[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return lastIdx;
|
||||
}
|
||||
}
|
||||
|
33
public/app/plugins/panel/geomap/utils/getLayersExtent.ts
Normal file
33
public/app/plugins/panel/geomap/utils/getLayersExtent.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Collection } from 'ol';
|
||||
import { createEmpty, extend, Extent } from 'ol/extent';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
|
||||
export function getLayersExtent(layers: Collection<BaseLayer>): Extent {
|
||||
return layers
|
||||
.getArray()
|
||||
.filter((l) => l instanceof VectorLayer || l instanceof LayerGroup)
|
||||
.flatMap((l) => {
|
||||
if (l instanceof LayerGroup) {
|
||||
return getLayerGroupExtent(l);
|
||||
} else if (l instanceof VectorLayer) {
|
||||
return l.getSource().getExtent() ?? [];
|
||||
}
|
||||
})
|
||||
.reduce(extend, createEmpty());
|
||||
}
|
||||
|
||||
export function getLayerGroupExtent(lg: LayerGroup) {
|
||||
return lg
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.filter((l) => l instanceof VectorLayer)
|
||||
.map((l) => {
|
||||
if (l instanceof VectorLayer) {
|
||||
return l.getSource().getExtent() ?? [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user