mirror of https://github.com/grafana/grafana
Geomap: styleConfig cleanup and symbol caching (#41622)
Co-authored-by: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com>pull/41670/head
parent
dfa14e9500
commit
862054918d
@ -0,0 +1,257 @@ |
||||
import { Fill, RegularShape, Stroke, Circle, Style, Icon } from 'ol/style'; |
||||
import { Registry, RegistryItem } from '@grafana/data'; |
||||
import { DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types'; |
||||
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; |
||||
import tinycolor from 'tinycolor2'; |
||||
|
||||
interface SymbolMaker extends RegistryItem { |
||||
aliasIds: string[]; |
||||
make: StyleMaker; |
||||
} |
||||
|
||||
enum RegularShapeId { |
||||
circle = 'circle', |
||||
square = 'square', |
||||
triangle = 'triangle', |
||||
star = 'star', |
||||
cross = 'cross', |
||||
x = 'x', |
||||
} |
||||
|
||||
const MarkerShapePath = { |
||||
circle: 'img/icons/marker/circle.svg', |
||||
square: 'img/icons/marker/square.svg', |
||||
triangle: 'img/icons/marker/triangle.svg', |
||||
star: 'img/icons/marker/star.svg', |
||||
cross: 'img/icons/marker/cross.svg', |
||||
x: 'img/icons/marker/x-mark.svg', |
||||
}; |
||||
|
||||
export function getFillColor(cfg: StyleConfigValues) { |
||||
const opacity = cfg.opacity == null ? 0.8 : cfg.opacity; |
||||
if (opacity === 1) { |
||||
return new Fill({ color: cfg.color }); |
||||
} |
||||
if (opacity > 0) { |
||||
const color = tinycolor(cfg.color).setAlpha(opacity).toRgbString(); |
||||
return new Fill({ color }); |
||||
} |
||||
return undefined; |
||||
} |
||||
|
||||
export const circleMarker = (cfg: StyleConfigValues) => { |
||||
return new Style({ |
||||
image: new Circle({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
fill: getFillColor(cfg), |
||||
radius: cfg.size ?? DEFAULT_SIZE, |
||||
}), |
||||
}); |
||||
}; |
||||
|
||||
// Square and cross
|
||||
const errorMarker = (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
const stroke = new Stroke({ color: '#F00', width: 1 }); |
||||
return [ |
||||
new Style({ |
||||
image: new RegularShape({ |
||||
stroke, |
||||
points: 4, |
||||
radius, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}), |
||||
new Style({ |
||||
image: new RegularShape({ |
||||
stroke, |
||||
points: 4, |
||||
radius, |
||||
radius2: 0, |
||||
angle: 0, |
||||
}), |
||||
}), |
||||
]; |
||||
}; |
||||
|
||||
const makers: SymbolMaker[] = [ |
||||
{ |
||||
id: RegularShapeId.circle, |
||||
name: 'Circle', |
||||
aliasIds: [MarkerShapePath.circle], |
||||
make: circleMarker, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.square, |
||||
name: 'Square', |
||||
aliasIds: [MarkerShapePath.square], |
||||
make: (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
fill: getFillColor(cfg), |
||||
points: 4, |
||||
radius, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.triangle, |
||||
name: 'Triangle', |
||||
aliasIds: [MarkerShapePath.triangle], |
||||
make: (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
fill: getFillColor(cfg), |
||||
points: 3, |
||||
radius, |
||||
rotation: Math.PI / 4, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.star, |
||||
name: 'Star', |
||||
aliasIds: [MarkerShapePath.star], |
||||
make: (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
fill: getFillColor(cfg), |
||||
points: 5, |
||||
radius, |
||||
radius2: radius * 0.4, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.cross, |
||||
name: 'Cross', |
||||
aliasIds: [MarkerShapePath.cross], |
||||
make: (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
points: 4, |
||||
radius, |
||||
radius2: 0, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.x, |
||||
name: 'X', |
||||
aliasIds: [MarkerShapePath.x], |
||||
make: (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }), |
||||
points: 4, |
||||
radius, |
||||
radius2: 0, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
async function prepareSVG(url: string): Promise<string> { |
||||
return fetch(url, { method: 'GET' }) |
||||
.then((res) => { |
||||
return res.text(); |
||||
}) |
||||
.then((text) => { |
||||
const parser = new DOMParser(); |
||||
const doc = parser.parseFromString(text, 'image/svg+xml'); |
||||
const svg = doc.getElementsByTagName('svg')[0]; |
||||
if (!svg) { |
||||
return ''; |
||||
} |
||||
// open layers requires a white fill becaues it uses tint to set color
|
||||
svg.setAttribute('fill', '#fff'); |
||||
const svgString = new XMLSerializer().serializeToString(svg); |
||||
const svgURI = encodeURIComponent(svgString); |
||||
return `data:image/svg+xml,${svgURI}`; |
||||
}) |
||||
.catch((error) => { |
||||
console.error(error); |
||||
return ''; |
||||
}); |
||||
} |
||||
|
||||
// Really just a cache for the various symbol styles
|
||||
const markerMakers = new Registry<SymbolMaker>(() => makers); |
||||
|
||||
export function getMarkerAsPath(shape?: string): string | undefined { |
||||
const marker = markerMakers.getIfExists(shape); |
||||
if (marker?.aliasIds?.length) { |
||||
return marker.aliasIds[0]; |
||||
} |
||||
return undefined; |
||||
} |
||||
|
||||
// Will prepare symbols as necessary
|
||||
export async function getMarkerMaker(symbol?: string): Promise<StyleMaker> { |
||||
if (!symbol) { |
||||
return circleMarker; |
||||
} |
||||
|
||||
let maker = markerMakers.getIfExists(symbol); |
||||
if (maker) { |
||||
return maker.make; |
||||
} |
||||
|
||||
// Prepare svg as icon
|
||||
if (symbol.endsWith('.svg')) { |
||||
const src = await prepareSVG(getPublicOrAbsoluteUrl(symbol)); |
||||
maker = { |
||||
id: symbol, |
||||
name: symbol, |
||||
aliasIds: [], |
||||
make: src |
||||
? (cfg: StyleConfigValues) => { |
||||
const radius = cfg.size ?? DEFAULT_SIZE; |
||||
return [ |
||||
new Style({ |
||||
image: new Icon({ |
||||
src, |
||||
color: cfg.color, |
||||
opacity: cfg.opacity ?? 1, |
||||
scale: (DEFAULT_SIZE + radius) / 100, |
||||
}), |
||||
}), |
||||
// transparent bounding box for featureAtPixel detection
|
||||
new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: 'rgba(0,0,0,0)' }), |
||||
points: 4, |
||||
radius: cfg.size, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}), |
||||
]; |
||||
} |
||||
: errorMarker, |
||||
}; |
||||
markerMakers.register(maker); |
||||
return maker.make; |
||||
} |
||||
|
||||
// defatult to showing a circle
|
||||
return errorMarker; |
||||
} |
@ -0,0 +1,16 @@ |
||||
import { Style, Text } from 'ol/style'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { StyleConfigValues, StyleMaker } from './types'; |
||||
import { getFillColor } from './markers'; |
||||
|
||||
export const textMarkerMaker: StyleMaker = (cfg: StyleConfigValues) => { |
||||
const fontFamily = config.theme2.typography.fontFamily; |
||||
const fontSize = cfg.size ?? 12; |
||||
return new Style({ |
||||
text: new Text({ |
||||
text: cfg.text ?? '?', |
||||
fill: getFillColor(cfg), |
||||
font: `normal ${fontSize}px ${fontFamily}`, |
||||
}), |
||||
}); |
||||
}; |
@ -0,0 +1,81 @@ |
||||
import { |
||||
ColorDimensionConfig, |
||||
ResourceDimensionConfig, |
||||
ResourceDimensionMode, |
||||
ScaleDimensionConfig, |
||||
TextDimensionConfig, |
||||
} from 'app/features/dimensions'; |
||||
import { Style } from 'ol/style'; |
||||
|
||||
export enum GeometryTypeId { |
||||
Point = 'point', |
||||
Line = 'line', |
||||
Polygon = 'polygon', |
||||
Any = '*any*', |
||||
} |
||||
|
||||
// StyleConfig is saved in panel json and is used to configure how items get rendered
|
||||
export interface StyleConfig { |
||||
color?: ColorDimensionConfig; |
||||
opacity?: number; // defaults to 80%
|
||||
|
||||
// For non-points
|
||||
lineWidth?: number; |
||||
|
||||
// Used for points and dynamic text
|
||||
size?: ScaleDimensionConfig; |
||||
symbol?: ResourceDimensionConfig; |
||||
|
||||
// Can show markers and text together!
|
||||
text?: TextDimensionConfig; |
||||
textConfig?: TextStyleConfig; |
||||
} |
||||
|
||||
export const DEFAULT_SIZE = 5; |
||||
|
||||
export const defaultStyleConfig = Object.freeze({ |
||||
size: { |
||||
fixed: DEFAULT_SIZE, |
||||
min: 2, |
||||
max: 15, |
||||
}, |
||||
color: { |
||||
fixed: 'dark-green', // picked from theme
|
||||
}, |
||||
opacity: 0.4, |
||||
symbol: { |
||||
mode: ResourceDimensionMode.Fixed, |
||||
fixed: 'img/icons/marker/circle.svg', |
||||
}, |
||||
}); |
||||
|
||||
/** |
||||
* Static options for text display. See: |
||||
* https://openlayers.org/en/latest/apidoc/module-ol_style_Text.html
|
||||
*/ |
||||
export interface TextStyleConfig { |
||||
fontSize?: number; |
||||
offsetX?: number; |
||||
offsetY?: number; |
||||
align?: 'left' | 'right' | 'center'; |
||||
baseline?: 'bottom' | 'top' | 'middle'; |
||||
} |
||||
|
||||
// Applying the config to real data gives the values
|
||||
export interface StyleConfigValues { |
||||
color: string; |
||||
opacity?: number; |
||||
lineWidth?: number; |
||||
size?: number; |
||||
symbol?: string; // the point symbol
|
||||
rotation?: number; |
||||
text?: string; |
||||
|
||||
// Pass though (not value dependant)
|
||||
textConfig?: TextStyleConfig; |
||||
} |
||||
|
||||
/** |
||||
* Given values create a style |
||||
*/ |
||||
export type StyleMaker = (values: StyleConfigValues) => Style | Style[]; |
@ -0,0 +1,18 @@ |
||||
import { StyleConfig } from './types'; |
||||
|
||||
/** Return a distinct list of fields used to dynamically change the style */ |
||||
export function getDependantFields(config: StyleConfig): Set<string> | undefined { |
||||
const fields = new Set<string>(); |
||||
|
||||
if (config.color?.field) { |
||||
fields.add(config.color.field); |
||||
} |
||||
if (config.size?.field) { |
||||
fields.add(config.size.field); |
||||
} |
||||
if (config.text?.field) { |
||||
fields.add(config.text.field); |
||||
} |
||||
|
||||
return fields; |
||||
} |
@ -1,34 +0,0 @@ |
||||
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; |
||||
|
||||
const getUri = (url: string, size: number): Promise<string> => { |
||||
return fetch(url, { method: 'GET' }) |
||||
.then((res) => { |
||||
return res.text(); |
||||
}) |
||||
.then((text) => { |
||||
const parser = new DOMParser(); |
||||
const doc = parser.parseFromString(text, 'image/svg+xml'); |
||||
const svg = doc.getElementsByTagName('svg')[0]; |
||||
if (!svg) { |
||||
return ''; |
||||
} |
||||
//set to white so ol color tint works
|
||||
svg.setAttribute('fill', '#fff'); |
||||
const svgString = new XMLSerializer().serializeToString(svg); |
||||
const svgURI = encodeURIComponent(svgString); |
||||
return `data:image/svg+xml,${svgURI}`; |
||||
}) |
||||
.catch((error) => { |
||||
console.error(error); |
||||
return ''; |
||||
}); |
||||
}; |
||||
|
||||
export const getSVGUri = async (url: string, size: number) => { |
||||
const svgURI = await getUri(url, size); |
||||
|
||||
if (!svgURI) { |
||||
return getPublicOrAbsoluteUrl('img/icons/marker/circle.svg'); |
||||
} |
||||
return svgURI; |
||||
}; |
@ -1,147 +0,0 @@ |
||||
import { Fill, RegularShape, Stroke, Style, Circle } from 'ol/style'; |
||||
import { Registry, RegistryItem } from '@grafana/data'; |
||||
import { StyleMaker, StyleMakerConfig } from '../types'; |
||||
export interface MarkerMaker extends RegistryItem { |
||||
// path to icon that will be shown (but then replaced)
|
||||
aliasIds: string[]; |
||||
make: StyleMaker; |
||||
hasFill: boolean; |
||||
} |
||||
|
||||
export enum RegularShapeId { |
||||
circle = 'circle', |
||||
square = 'square', |
||||
triangle = 'triangle', |
||||
star = 'star', |
||||
cross = 'cross', |
||||
x = 'x', |
||||
} |
||||
|
||||
const MarkerShapePath = { |
||||
circle: 'img/icons/marker/circle.svg', |
||||
square: 'img/icons/marker/square.svg', |
||||
triangle: 'img/icons/marker/triangle.svg', |
||||
star: 'img/icons/marker/star.svg', |
||||
cross: 'img/icons/marker/cross.svg', |
||||
x: 'img/icons/marker/x-mark.svg', |
||||
}; |
||||
|
||||
export const circleMarker: MarkerMaker = { |
||||
id: RegularShapeId.circle, |
||||
name: 'Circle', |
||||
hasFill: true, |
||||
aliasIds: [MarkerShapePath.circle], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new Circle({ |
||||
stroke: new Stroke({ color: cfg.color }), |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
radius: cfg.size, |
||||
}), |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
const makers: MarkerMaker[] = [ |
||||
circleMarker, |
||||
{ |
||||
id: RegularShapeId.square, |
||||
name: 'Square', |
||||
hasFill: true, |
||||
aliasIds: [MarkerShapePath.square], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
stroke: new Stroke({ color: cfg.color, width: 1 }), |
||||
points: 4, |
||||
radius: cfg.size, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.triangle, |
||||
name: 'Triangle', |
||||
hasFill: true, |
||||
aliasIds: [MarkerShapePath.triangle], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
stroke: new Stroke({ color: cfg.color, width: 1 }), |
||||
points: 3, |
||||
radius: cfg.size, |
||||
rotation: Math.PI / 4, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.star, |
||||
name: 'Star', |
||||
hasFill: true, |
||||
aliasIds: [MarkerShapePath.star], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
stroke: new Stroke({ color: cfg.color, width: 1 }), |
||||
points: 5, |
||||
radius: cfg.size, |
||||
radius2: cfg.size * 0.4, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.cross, |
||||
name: 'Cross', |
||||
hasFill: false, |
||||
aliasIds: [MarkerShapePath.cross], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
stroke: new Stroke({ color: cfg.color, width: 1 }), |
||||
points: 4, |
||||
radius: cfg.size, |
||||
radius2: 0, |
||||
angle: 0, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
id: RegularShapeId.x, |
||||
name: 'X', |
||||
hasFill: false, |
||||
aliasIds: [MarkerShapePath.x], |
||||
make: (cfg: StyleMakerConfig) => { |
||||
return new Style({ |
||||
image: new RegularShape({ |
||||
fill: new Fill({ color: cfg.fillColor }), |
||||
stroke: new Stroke({ color: cfg.color, width: 1 }), |
||||
points: 4, |
||||
radius: cfg.size, |
||||
radius2: 0, |
||||
angle: Math.PI / 4, |
||||
}), |
||||
}); |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
export const markerMakers = new Registry<MarkerMaker>(() => makers); |
||||
|
||||
export const getMarkerFromPath = (svgPath: string): MarkerMaker | undefined => { |
||||
for (const [key, val] of Object.entries(MarkerShapePath)) { |
||||
if (val === svgPath) { |
||||
return markerMakers.getIfExists(key); |
||||
} |
||||
} |
||||
return undefined; |
||||
}; |
Loading…
Reference in new issue