Geomap: WebGL for Marker Layer (#95457)
* Geomap: Implement webgl for marker layer * Cover rgb color formatting * Adjust size * Adjust size and leave todo for adjustment * Add custom icon example * Use prepareSVG to handle custom icons * Apply icon offset * Add example of text labels for fixed case * Add text canvas layer and layerGroup * Use textMarker for text layer style * Fix geometry calcs * Only include text layer if needed * Remove extra line * Move color functions to utils * Pass webGL bool to marker maker * Move webgl symbol style to marker maker * Get hit detection working for webgl layers * Improve icon hit detection * Improve text performance * Fix types and simplify webgl styling * Simplify webgl regular shape lookup * Add comments * Set fallback color to white * Fix data fit for initial view * Update color utils to support colors with alpha * Add tests for color value function * Add tests for getWebGLStyle function * Clean up comments * Only show text if no symbol is specified * Remove size multiplier from webgl implementation * Add size multiplier back for now
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import Map from 'ol/Map';
|
||||
import VectorImage from 'ol/layer/VectorImage';
|
||||
import { Point } from 'ol/geom';
|
||||
import { VectorImage } from 'ol/layer';
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import {
|
||||
MapLayerRegistryItem,
|
||||
@@ -19,9 +21,10 @@ import { getLocationMatchers } from 'app/features/geo/utils/location';
|
||||
import { MarkersLegend, MarkersLegendProps } from '../../components/MarkersLegend';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { StyleEditor } from '../../editor/StyleEditor';
|
||||
import { defaultStyleConfig, StyleConfig } from '../../style/types';
|
||||
import { getStyleConfigState } from '../../style/utils';
|
||||
import { getStyleDimension} from '../../utils/utils';
|
||||
import { getWebGLStyle, textMarker } from '../../style/markers';
|
||||
import { DEFAULT_SIZE, defaultStyleConfig, StyleConfig } from '../../style/types';
|
||||
import { getDisplacement, getRGBValues, getStyleConfigState, styleUsesText } from '../../style/utils';
|
||||
import { getStyleDimension } from '../../utils/utils';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface MarkersConfig {
|
||||
@@ -72,11 +75,16 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
};
|
||||
|
||||
const style = await getStyleConfigState(config.style);
|
||||
const symbol = config.style.symbol?.fixed;
|
||||
const webGLStyle = await getWebGLStyle(symbol, config.style.opacity);
|
||||
const hasText = styleUsesText(config.style);
|
||||
const location = await getLocationMatchers(options.location);
|
||||
const source = new FrameVectorSource(location);
|
||||
const vectorLayer = new VectorImage({
|
||||
source,
|
||||
declutter: false // TODO consider making this an option or explore grouping strategies
|
||||
const source = new FrameVectorSource<Point>(location);
|
||||
const symbolLayer = new WebGLPointsLayer({ source, style: webGLStyle });
|
||||
const textLayer = new VectorImage({ source, declutter: true });
|
||||
const layers = new LayerGroup({
|
||||
// If text and no symbol, only show text - fall back on default symbol
|
||||
layers: hasText && symbol ? [symbolLayer, textLayer] : hasText && !symbol ? [textLayer] : [symbolLayer],
|
||||
});
|
||||
|
||||
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
|
||||
@@ -85,37 +93,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||
}
|
||||
|
||||
if (!style.fields) {
|
||||
// Set a global style
|
||||
vectorLayer.setStyle(style.maker(style.base));
|
||||
} else {
|
||||
vectorLayer.setStyle((feature: FeatureLike) => {
|
||||
const idx: number = feature.get('rowIndex');
|
||||
const dims = style.dims;
|
||||
if (!dims || !isNumber(idx)) {
|
||||
return style.maker(style.base);
|
||||
}
|
||||
|
||||
const values = { ...style.base };
|
||||
|
||||
if (dims.color) {
|
||||
values.color = dims.color.get(idx);
|
||||
}
|
||||
if (dims.size) {
|
||||
values.size = dims.size.get(idx);
|
||||
}
|
||||
if (dims.text) {
|
||||
values.text = dims.text.get(idx);
|
||||
}
|
||||
if (dims.rotation) {
|
||||
values.rotation = dims.rotation.get(idx);
|
||||
}
|
||||
return style.maker(values);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
init: () => layers,
|
||||
legend: legend,
|
||||
update: (data: PanelData) => {
|
||||
if (!data.series?.length) {
|
||||
@@ -132,11 +111,50 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
styleConfig: style,
|
||||
size: style.dims?.size,
|
||||
layerName: options.name,
|
||||
layer: vectorLayer,
|
||||
layer: symbolLayer,
|
||||
});
|
||||
}
|
||||
|
||||
source.update(frame);
|
||||
source.forEachFeature((feature) => {
|
||||
const idx: number = feature.get('rowIndex');
|
||||
const dims = style.dims;
|
||||
const values = { ...style.base };
|
||||
|
||||
if (dims?.color) {
|
||||
values.color = dims.color.get(idx);
|
||||
}
|
||||
if (dims?.size) {
|
||||
values.size = dims.size.get(idx);
|
||||
}
|
||||
if (dims?.text) {
|
||||
values.text = dims.text.get(idx);
|
||||
}
|
||||
if (dims?.rotation) {
|
||||
values.rotation = dims.rotation.get(idx);
|
||||
}
|
||||
const colorString = tinycolor(theme.visualization.getColorByName(values.color)).toString();
|
||||
const colorValues = getRGBValues(colorString);
|
||||
|
||||
const radius = values.size ?? DEFAULT_SIZE;
|
||||
const displacement = getDisplacement(values.symbolAlign ?? defaultStyleConfig.symbolAlign, radius);
|
||||
|
||||
// WebGLPointsLayer uses style expressions instead of style functions
|
||||
feature.setProperties({ red: colorValues?.r ?? 255 });
|
||||
feature.setProperties({ green: colorValues?.g ?? 255 });
|
||||
feature.setProperties({ blue: colorValues?.b ?? 255 });
|
||||
feature.setProperties({ size: (values.size ?? 1) * 2 }); // TODO unify sizing across all source types
|
||||
feature.setProperties({ rotation: ((values.rotation ?? 0) * Math.PI) / 180 });
|
||||
feature.setProperties({ opacity: (values.opacity ?? 1) * (colorValues?.a ?? 1) });
|
||||
feature.setProperties({ offsetX: displacement[0] });
|
||||
feature.setProperties({ offsetY: displacement[1] });
|
||||
|
||||
// Set style to be used by VectorLayer (text only)
|
||||
if (hasText) {
|
||||
const textStyle = textMarker(values);
|
||||
feature.setStyle(textStyle);
|
||||
}
|
||||
});
|
||||
break; // Only the first frame for now!
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
|
||||
|
||||
import { getWebGLStyle } from './markers';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('app/features/dimensions', () => ({
|
||||
getPublicOrAbsoluteUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getWebGLStyle', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns default circle style when no symbol is provided', async () => {
|
||||
const result = await getWebGLStyle();
|
||||
expect(result).toEqual({
|
||||
symbol: {
|
||||
symbolType: 'circle',
|
||||
size: ['get', 'size', 'number'],
|
||||
color: ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue']],
|
||||
offset: ['array', ['get', 'offsetX'], ['get', 'offsetY']],
|
||||
rotation: ['get', 'rotation', 'number'],
|
||||
opacity: ['get', 'opacity', 'number'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns circle style for known WebGL regular shape', async () => {
|
||||
const result = await getWebGLStyle('img/icons/marker/circle.svg');
|
||||
if (result.symbol) {
|
||||
expect(result.symbol.symbolType).toBe('circle');
|
||||
expect(result.symbol).not.toHaveProperty('src');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns square style for known WebGL regular shape', async () => {
|
||||
const result = await getWebGLStyle('img/icons/marker/square.svg');
|
||||
if (result.symbol) {
|
||||
expect(result.symbol.symbolType).toBe('square');
|
||||
expect(result.symbol).not.toHaveProperty('src');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns triangle style for known WebGL regular shape', async () => {
|
||||
const result = await getWebGLStyle('img/icons/marker/triangle.svg');
|
||||
if (result.symbol) {
|
||||
expect(result.symbol.symbolType).toBe('triangle');
|
||||
expect(result.symbol).not.toHaveProperty('src');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns image style with src for custom SVG symbol', async () => {
|
||||
(getPublicOrAbsoluteUrl as jest.Mock).mockReturnValue('test.svg');
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve('<svg width="100" height="100"></svg>'),
|
||||
// Add minimal Response properties to satisfy TypeScript
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
} as Response)
|
||||
);
|
||||
const result = await getWebGLStyle('test.svg');
|
||||
if (result.symbol) {
|
||||
expect(result.symbol.symbolType).toBe('image');
|
||||
expect(result.symbol.src).toContain('data:image/svg+xml');
|
||||
}
|
||||
});
|
||||
|
||||
it('includes background circle with opacity-adjusted stroke when opacity is provided', async () => {
|
||||
(getPublicOrAbsoluteUrl as jest.Mock).mockReturnValue('custom.svg');
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve('<svg width="100" height="100" viewBox="0 0 100 100"></svg>'),
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
} as Response)
|
||||
);
|
||||
const result = await getWebGLStyle('custom.svg', 0.5);
|
||||
if (result.symbol?.src) {
|
||||
expect(result.symbol.symbolType).toBe('image');
|
||||
expect(result.symbol.src).toContain('circle');
|
||||
const decodedSrc = decodeURIComponent(result.symbol.src);
|
||||
expect(decodedSrc).toContain('stroke="rgba(255,255,255,0.2)"'); // 0.1 / 0.5 = 0.2
|
||||
}
|
||||
});
|
||||
|
||||
it('handles fetch error gracefully', async () => {
|
||||
// Mock console.error to suppress output and verify it's called
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
(getPublicOrAbsoluteUrl as jest.Mock).mockReturnValue('error.svg');
|
||||
global.fetch = jest.fn(() => Promise.reject(new Error('Fetch failed')));
|
||||
const result = await getWebGLStyle('error.svg');
|
||||
if (result.symbol) {
|
||||
expect(result.symbol.symbolType).toBe('image');
|
||||
expect(result.symbol.src).toBe(''); // Empty SVG
|
||||
}
|
||||
// Verify console.error was called with the expected error
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Fetch failed'));
|
||||
|
||||
// Clean up the spy
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style';
|
||||
import { LiteralStyle } from 'ol/style/literal';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { Registry, RegistryItem, textUtil } from '@grafana/data';
|
||||
@@ -31,6 +32,12 @@ const MarkerShapePath = {
|
||||
x: 'img/icons/marker/x-mark.svg',
|
||||
};
|
||||
|
||||
const WebGLRegularShapes: Record<string, string> = {
|
||||
circle: 'img/icons/marker/circle.svg',
|
||||
square: 'img/icons/marker/square.svg',
|
||||
triangle: 'img/icons/marker/triangle.svg',
|
||||
};
|
||||
|
||||
export function getFillColor(cfg: StyleConfigValues) {
|
||||
const opacity = cfg.opacity == null ? 0.8 : cfg.opacity;
|
||||
if (opacity === 1) {
|
||||
@@ -251,7 +258,7 @@ const makers: SymbolMaker[] = [
|
||||
},
|
||||
];
|
||||
|
||||
async function prepareSVG(url: string, size?: number): Promise<string> {
|
||||
async function prepareSVG(url: string, size?: number, backgroundOpacity?: number): Promise<string> {
|
||||
return fetch(url, { method: 'GET' })
|
||||
.then((res) => {
|
||||
return res.text();
|
||||
@@ -274,6 +281,23 @@ async function prepareSVG(url: string, size?: number): Promise<string> {
|
||||
svg.setAttribute('fill', '#fff');
|
||||
svg.setAttribute('width', `${width}px`);
|
||||
svg.setAttribute('height', `${height}px`);
|
||||
|
||||
// add a mostly transparent circle behind the icon for webGL hit detection
|
||||
// TODO open layers discards fully transparent elements for hit detection
|
||||
if (backgroundOpacity) {
|
||||
const viewBox = svg.getAttribute('viewBox')?.split(' ') ?? [0, 0, width, height];
|
||||
const viewCenterX = Number(viewBox[2]) / 2;
|
||||
const viewCenterY = Number(viewBox[3]) / 2;
|
||||
const circleElement = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circleElement.setAttribute('cx', viewCenterX.toString());
|
||||
circleElement.setAttribute('cy', viewCenterY.toString());
|
||||
circleElement.setAttribute('fill', 'none');
|
||||
circleElement.setAttribute('r', (viewCenterX / 2).toString());
|
||||
circleElement.setAttribute('stroke', `rgba(255,255,255,${backgroundOpacity})`);
|
||||
circleElement.setAttribute('stroke-width', viewCenterX.toString());
|
||||
svg.prepend(circleElement);
|
||||
}
|
||||
|
||||
const svgString = new XMLSerializer().serializeToString(svg);
|
||||
const svgURI = encodeURIComponent(svgString);
|
||||
return `data:image/svg+xml,${svgURI}`;
|
||||
@@ -295,6 +319,36 @@ export function getMarkerAsPath(shape?: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Returns literal style for WebGL markers
|
||||
export async function getWebGLStyle(symbol?: string, opacity?: number): Promise<LiteralStyle> {
|
||||
// style expressions
|
||||
const symbolStyle: LiteralStyle = {
|
||||
symbol: {
|
||||
symbolType: 'circle',
|
||||
size: ['get', 'size', 'number'],
|
||||
color: ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue']],
|
||||
offset: ['array', ['get', 'offsetX'], ['get', 'offsetY']],
|
||||
rotation: ['get', 'rotation', 'number'],
|
||||
opacity: ['get', 'opacity', 'number'],
|
||||
},
|
||||
};
|
||||
// set symbolType and src if a symbol is provided
|
||||
if (symbol && symbolStyle.symbol) {
|
||||
const imageString = 'image';
|
||||
const symbolType = Object.keys(WebGLRegularShapes).find((key) => WebGLRegularShapes[key] === symbol) ?? imageString;
|
||||
symbolStyle.symbol = { ...symbolStyle.symbol, symbolType };
|
||||
if (symbolType === imageString) {
|
||||
const backgroundOpacity = opacity === 0 ? 0 : 0.1 / (opacity ?? 1);
|
||||
symbolStyle.symbol = {
|
||||
...symbolStyle.symbol,
|
||||
src: await prepareSVG(getPublicOrAbsoluteUrl(symbol), undefined, backgroundOpacity),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return symbolStyle;
|
||||
}
|
||||
|
||||
// Will prepare symbols as necessary
|
||||
export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): Promise<StyleMaker> {
|
||||
if (!symbol) {
|
||||
|
||||
@@ -171,3 +171,10 @@ export interface StyleConfigState {
|
||||
* Given values create a style
|
||||
*/
|
||||
export type StyleMaker = (values: StyleConfigValues) => Style | Style[];
|
||||
|
||||
export interface ColorValue {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a?: number;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ResourceDimensionMode } from '@grafana/schema';
|
||||
|
||||
import { HorizontalAlign, VerticalAlign, StyleConfig, SymbolAlign } from './types';
|
||||
import { getDisplacement, getStyleConfigState } from './utils';
|
||||
import { getDisplacement, getRGBValues, getStyleConfigState } from './utils';
|
||||
|
||||
describe('style utils', () => {
|
||||
it('should fill in default values', async () => {
|
||||
@@ -74,4 +74,24 @@ describe('style utils', () => {
|
||||
const displacement = getDisplacement(symbolAlign, radius);
|
||||
expect(displacement).toEqual([0, 0]);
|
||||
});
|
||||
it('should return correct color values for hex default color', async () => {
|
||||
const colorString = '#37872d';
|
||||
const colorValues = getRGBValues(colorString);
|
||||
expect(colorValues).toEqual({ r: 55, g: 135, b: 45 });
|
||||
});
|
||||
it('should return correct color values for rgb color', async () => {
|
||||
const colorString = 'rgb(242, 73, 92)';
|
||||
const colorValues = getRGBValues(colorString);
|
||||
expect(colorValues).toEqual({ r: 242, g: 73, b: 92 });
|
||||
});
|
||||
it('should return correct color values for rgba color', async () => {
|
||||
const colorString = 'rgba(90, 0, 135, 0.5)';
|
||||
const colorValues = getRGBValues(colorString);
|
||||
expect(colorValues).toEqual({ r: 90, g: 0, b: 135, a: 0.5 });
|
||||
});
|
||||
it('should return correct color values for transparent color', async () => {
|
||||
const colorString = 'rgba(0, 0, 0, 0)';
|
||||
const colorValues = getRGBValues(colorString);
|
||||
expect(colorValues).toEqual({ r: 0, g: 0, b: 0, a: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
StyleConfigFields,
|
||||
StyleConfigState,
|
||||
SymbolAlign,
|
||||
ColorValue,
|
||||
} from './types';
|
||||
|
||||
/** Indicate if the style wants to show text values */
|
||||
@@ -91,3 +92,60 @@ export function getDisplacement(symbolAlign: SymbolAlign, radius: number) {
|
||||
}
|
||||
return displacement;
|
||||
}
|
||||
|
||||
export function getRGBValues(colorString: string): ColorValue | null {
|
||||
// Check if it's a hex color
|
||||
if (colorString.startsWith('#')) {
|
||||
return getRGBFromHex(colorString);
|
||||
}
|
||||
|
||||
// Check if it's an RGB color
|
||||
else if (colorString.startsWith('rgb')) {
|
||||
return getRGBFromRGBString(colorString);
|
||||
}
|
||||
|
||||
// Handle other color formats if needed
|
||||
else {
|
||||
console.warn(`Unsupported color format: ${colorString}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getRGBFromHex(hexColor: string): ColorValue {
|
||||
// Remove the '#' character
|
||||
hexColor = hexColor.slice(1);
|
||||
|
||||
// Convert hex to decimal values
|
||||
const r = parseInt(hexColor.slice(0, 2), 16);
|
||||
const g = parseInt(hexColor.slice(2, 4), 16);
|
||||
const b = parseInt(hexColor.slice(4, 6), 16);
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function getRGBFromRGBString(rgbString: string): ColorValue | null {
|
||||
// Use regex to extract the numbers, supporting both rgb(r,g,b) and rgba(r,g,b,a) formats
|
||||
const matches = rgbString.match(/\d+\.?\d*/g);
|
||||
|
||||
if (matches) {
|
||||
if (matches.length === 3) {
|
||||
return {
|
||||
r: parseInt(matches[0], 10),
|
||||
g: parseInt(matches[1], 10),
|
||||
b: parseInt(matches[2], 10),
|
||||
};
|
||||
} else if (matches.length === 4) {
|
||||
return {
|
||||
r: parseInt(matches[0], 10),
|
||||
g: parseInt(matches[1], 10),
|
||||
b: parseInt(matches[2], 10),
|
||||
a: parseFloat(matches[3]), // Using parseFloat for alpha as it can be decimal (0-1)
|
||||
};
|
||||
} else {
|
||||
console.warn(`Unsupported color format: ${rgbString}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unsupported color format: ${rgbString}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createEmpty, extend, Extent } from 'ol/extent';
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorImage from 'ol/layer/VectorImage';
|
||||
import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';
|
||||
|
||||
import { MapLayerState } from '../types';
|
||||
|
||||
@@ -51,9 +52,9 @@ export function getLayerGroupExtent(lg: LayerGroup, lastOnly: boolean) {
|
||||
return lg
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.filter((l) => l instanceof VectorLayer || l instanceof VectorImage)
|
||||
.filter((l) => l instanceof VectorLayer || l instanceof VectorImage || l instanceof WebGLPointsLayer)
|
||||
.map((l) => {
|
||||
if (l instanceof VectorLayer || l instanceof VectorImage) {
|
||||
if (l instanceof VectorLayer || l instanceof VectorImage || l instanceof WebGLPointsLayer) {
|
||||
if (lastOnly) {
|
||||
// Return last coordinate only
|
||||
const feat = l.getSource().getFeatures();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Map as OpenLayersMap } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import LayerGroup from 'ol/layer/Group';
|
||||
import WebGLPointsLayer from 'ol/layer/WebGLPoints';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { getFrameMatchers, MapLayerHandler, MapLayerOptions, PanelData, textUtil } from '@grafana/data';
|
||||
@@ -149,6 +151,19 @@ export async function initLayer(
|
||||
// eslint-disable-next-line
|
||||
(state.layer as any).__state = state;
|
||||
|
||||
// Pass state into WebGLPointsLayers contained in a LayerGroup
|
||||
if (layer instanceof LayerGroup) {
|
||||
layer
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer) => {
|
||||
if (layer instanceof WebGLPointsLayer) {
|
||||
// eslint-disable-next-line
|
||||
(layer as any).__state = state;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyLayerFilter(handler, options, panel.props.data);
|
||||
|
||||
return state;
|
||||
|
||||
Reference in New Issue
Block a user