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, "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.", "15"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
[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"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
|
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[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 ScaleLine from 'ol/control/ScaleLine';
|
||||||
import Zoom from 'ol/control/Zoom';
|
import Zoom from 'ol/control/Zoom';
|
||||||
import { Coordinate } from 'ol/coordinate';
|
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 { defaults as interactionDefaults } from 'ol/interaction';
|
||||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||||
import BaseLayer from 'ol/layer/Base';
|
import BaseLayer from 'ol/layer/Base';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
|
||||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||||
import React, { Component, ReactNode } from 'react';
|
import React, { Component, ReactNode } from 'react';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -40,6 +39,7 @@ import { getGlobalStyles } from './globalStyles';
|
|||||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
||||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
|
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
|
||||||
|
import { getLayersExtent } from './utils/getLayersExtent';
|
||||||
import { centerPointRegistry, MapCenterID } from './view';
|
import { centerPointRegistry, MapCenterID } from './view';
|
||||||
|
|
||||||
// Allows multiple panels to share the same view instance
|
// 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) {
|
if (v.id === MapCenterID.Coordinates) {
|
||||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||||
} else if (v.id === MapCenterID.Fit) {
|
} else if (v.id === MapCenterID.Fit) {
|
||||||
var extent = layers
|
const extent = getLayersExtent(layers);
|
||||||
.getArray()
|
|
||||||
.filter((l) => l instanceof VectorLayer)
|
|
||||||
.map((l) => (l as VectorLayer<any>).getSource().getExtent() ?? [])
|
|
||||||
.reduce(extend, createEmpty());
|
|
||||||
if (!isEmpty(extent)) {
|
if (!isEmpty(extent)) {
|
||||||
view.fit(extent, {
|
view.fit(extent, {
|
||||||
padding: [30, 30, 30, 30],
|
padding: [30, 30, 30, 30],
|
||||||
|
@ -6,10 +6,15 @@ import {
|
|||||||
FrameGeometrySourceMode,
|
FrameGeometrySourceMode,
|
||||||
PluginState,
|
PluginState,
|
||||||
EventBus,
|
EventBus,
|
||||||
|
DataHoverEvent,
|
||||||
|
DataHoverClearEvent,
|
||||||
|
DataFrame,
|
||||||
|
TIME_SERIES_TIME_FIELD_NAME,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import Map from 'ol/Map';
|
import Map from 'ol/Map';
|
||||||
import { FeatureLike } from 'ol/Feature';
|
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 { getColorDimension } from 'app/features/dimensions';
|
||||||
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
||||||
import { StyleEditor } from './StyleEditor';
|
import { StyleEditor } from './StyleEditor';
|
||||||
@ -18,6 +23,11 @@ import VectorLayer from 'ol/layer/Vector';
|
|||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { routeStyle } from '../../style/markers';
|
import { routeStyle } from '../../style/markers';
|
||||||
import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource';
|
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
|
// Configuration options for Circle overlays
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
@ -70,9 +80,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
const style = await getStyleConfigState(config.style);
|
const style = await getStyleConfigState(config.style);
|
||||||
const location = await getLocationMatchers(options.location);
|
const location = await getLocationMatchers(options.location);
|
||||||
const source = new FrameVectorSource(location);
|
const source = new FrameVectorSource(location);
|
||||||
const vectorLayer = new VectorLayer({
|
const vectorLayer = new VectorLayer({ source });
|
||||||
source,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!style.fields) {
|
if (!style.fields) {
|
||||||
// Set a global style
|
// 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 {
|
return {
|
||||||
init: () => vectorLayer,
|
init: () => layer,
|
||||||
|
dispose: () => subscriptions.unsubscribe(),
|
||||||
update: (data: PanelData) => {
|
update: (data: PanelData) => {
|
||||||
if (!data.series?.length) {
|
if (!data.series?.length) {
|
||||||
return; // ignore empty
|
return; // ignore empty
|
||||||
@ -145,3 +215,34 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
// fill in the default values
|
// fill in the default values
|
||||||
defaultOptions,
|
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