The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/geomap/style/markers.ts

338 lines
9.1 KiB

import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style';
import tinycolor from 'tinycolor2';
import { Registry, RegistryItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types';
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 function getStrokeStyle(cfg: StyleConfigValues) {
const opacity = cfg.opacity == null ? 0.8 : cfg.opacity;
if (opacity === 1) {
return new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 });
}
if (opacity > 0) {
const color = tinycolor(cfg.color).setAlpha(opacity).toRgbString();
return new Stroke({ color, width: cfg.lineWidth ?? 1 });
}
return undefined;
}
const textLabel = (cfg: StyleConfigValues) => {
if (!cfg.text) {
return undefined;
}
const fontFamily = config.theme2.typography.fontFamily;
const textConfig = {
...defaultStyleConfig.textConfig,
...cfg.textConfig,
};
return new Text({
text: cfg.text,
fill: new Fill({ color: cfg.color ?? defaultStyleConfig.color.fixed }),
font: `normal ${textConfig.fontSize}px ${fontFamily}`,
...textConfig,
});
};
export const textMarker = (cfg: StyleConfigValues) => {
return new Style({
text: textLabel(cfg),
});
};
export const circleMarker = (cfg: StyleConfigValues) => {
const stroke = new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 });
return new Style({
image: new Circle({
stroke,
fill: getFillColor(cfg),
radius: cfg.size ?? DEFAULT_SIZE,
}),
text: textLabel(cfg),
stroke, // in case lines are sent to the markers layer
});
};
// Does not have image
export const polyStyle = (cfg: StyleConfigValues) => {
return new Style({
fill: getFillColor(cfg),
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
text: textLabel(cfg),
});
};
export const routeStyle = (cfg: StyleConfigValues) => {
return new Style({
fill: getFillColor(cfg),
stroke: getStrokeStyle(cfg),
text: textLabel(cfg),
});
};
// 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;
const rotation = cfg.rotation ?? 0;
return new Style({
image: new RegularShape({
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
fill: getFillColor(cfg),
points: 4,
radius,
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
}),
text: textLabel(cfg),
});
},
},
{
id: RegularShapeId.triangle,
name: 'Triangle',
aliasIds: [MarkerShapePath.triangle],
make: (cfg: StyleConfigValues) => {
const radius = cfg.size ?? DEFAULT_SIZE;
const rotation = cfg.rotation ?? 0;
return new Style({
image: new RegularShape({
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
fill: getFillColor(cfg),
points: 3,
radius,
rotation: (rotation * Math.PI) / 180,
angle: 0,
}),
text: textLabel(cfg),
});
},
},
{
id: RegularShapeId.star,
name: 'Star',
aliasIds: [MarkerShapePath.star],
make: (cfg: StyleConfigValues) => {
const radius = cfg.size ?? DEFAULT_SIZE;
const rotation = cfg.rotation ?? 0;
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,
rotation: (rotation * Math.PI) / 180,
}),
text: textLabel(cfg),
});
},
},
{
id: RegularShapeId.cross,
name: 'Cross',
aliasIds: [MarkerShapePath.cross],
make: (cfg: StyleConfigValues) => {
const radius = cfg.size ?? DEFAULT_SIZE;
const rotation = cfg.rotation ?? 0;
return new Style({
image: new RegularShape({
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
points: 4,
radius,
radius2: 0,
angle: 0,
rotation: (rotation * Math.PI) / 180,
}),
text: textLabel(cfg),
});
},
},
{
id: RegularShapeId.x,
name: 'X',
aliasIds: [MarkerShapePath.x],
make: (cfg: StyleConfigValues) => {
const radius = cfg.size ?? DEFAULT_SIZE;
const rotation = cfg.rotation ?? 0;
return new Style({
image: new RegularShape({
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
points: 4,
radius,
radius2: 0,
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
}),
text: textLabel(cfg),
});
},
},
];
async function prepareSVG(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 '';
}
const svgSize = size ?? 100;
const width = svg.getAttribute('width') ?? svgSize;
const height = svg.getAttribute('height') ?? svgSize;
// open layers requires a white fill becaues it uses tint to set color
svg.setAttribute('fill', '#fff');
svg.setAttribute('width', `${width}px`);
svg.setAttribute('height', `${height}px`);
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, hasTextLabel?: boolean): Promise<StyleMaker> {
if (!symbol) {
return hasTextLabel ? textMarker : 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;
const rotation = cfg.rotation ?? 0;
return [
new Style({
image: new Icon({
src,
color: cfg.color,
opacity: cfg.opacity ?? 1,
scale: (DEFAULT_SIZE + radius) / 100,
rotation: (rotation * Math.PI) / 180,
}),
text: !cfg?.text ? undefined : textLabel(cfg),
}),
// 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,
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
}),
}),
];
}
: errorMarker,
};
markerMakers.register(maker);
return maker.make;
}
// default to showing a circle
return errorMarker;
}