Geomap: Support shared crosshair for route layer (#51495)

This commit is contained in:
Alexander Zobnin 2022-07-06 10:08:55 +03:00 committed by GitHub
parent ffd9d9d0c5
commit 21591be469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 15 deletions

View File

@ -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"]

View File

@ -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],

View File

@ -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;
}
}

View 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 [];
}
});
}