mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: styleConfig cleanup and symbol caching (#41622)
Co-authored-by: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com>
This commit is contained in:
parent
dfa14e9500
commit
862054918d
@ -12,6 +12,7 @@ import { ComparisonOperation, FeatureStyleConfig } from '../../types';
|
||||
import { Stroke, Style } from 'ol/style';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor';
|
||||
import { circleMarker } from '../../style/markers';
|
||||
export interface GeoJSONMapperConfig {
|
||||
// URL for a geojson file
|
||||
src?: string;
|
||||
@ -77,6 +78,11 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
const vectorLayer = new VectorLayer({
|
||||
source,
|
||||
style: (feature: FeatureLike) => {
|
||||
const type = feature.getGeometry()?.getType();
|
||||
if (type === 'Point') {
|
||||
return circleMarker({color:DEFAULT_STYLE_RULE.fillColor});
|
||||
}
|
||||
|
||||
if (feature && config?.styles?.length) {
|
||||
for (const style of config.styles) {
|
||||
//check if there is no style rule or if the rule matches feature property
|
||||
@ -113,6 +119,7 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
options: [
|
||||
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
|
||||
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
|
||||
{ label: 'public/gazetteer/airports.geojson', value: 'public/gazetteer/airports.geojson' },
|
||||
],
|
||||
allowCustomValue: true,
|
||||
},
|
||||
|
@ -11,53 +11,29 @@ import Feature from 'ol/Feature';
|
||||
import { Point } from 'ol/geom';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
ScaleDimensionConfig,
|
||||
getScaledDimension,
|
||||
getColorDimension,
|
||||
ResourceDimensionConfig,
|
||||
ResourceDimensionMode,
|
||||
ResourceFolderName,
|
||||
getPublicOrAbsoluteUrl,
|
||||
} from 'app/features/dimensions';
|
||||
import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||
import { getMarkerFromPath } from '../../utils/regularShapes';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
|
||||
import { StyleMaker, StyleMakerConfig } from '../../types';
|
||||
import { getSVGUri } from '../../utils/prepareSVG';
|
||||
import { getMarkerMaker } from '../../style/markers';
|
||||
import { defaultStyleConfig, StyleConfig } from '../../style/types';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface MarkersConfig {
|
||||
size: ScaleDimensionConfig;
|
||||
color: ColorDimensionConfig;
|
||||
fillOpacity: number;
|
||||
style: StyleConfig;
|
||||
showLegend?: boolean;
|
||||
markerSymbol: ResourceDimensionConfig;
|
||||
}
|
||||
|
||||
const DEFAULT_SIZE = 5;
|
||||
|
||||
const defaultOptions: MarkersConfig = {
|
||||
size: {
|
||||
fixed: DEFAULT_SIZE,
|
||||
min: 2,
|
||||
max: 15,
|
||||
},
|
||||
color: {
|
||||
fixed: 'dark-green', // picked from theme
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
style: defaultStyleConfig,
|
||||
showLegend: true,
|
||||
markerSymbol: {
|
||||
mode: ResourceDimensionMode.Fixed,
|
||||
fixed: 'img/icons/marker/circle.svg',
|
||||
},
|
||||
};
|
||||
|
||||
export const MARKERS_LAYER_ID = 'markers';
|
||||
@ -100,10 +76,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||
}
|
||||
|
||||
const markerPath =
|
||||
getPublicOrAbsoluteUrl(config.markerSymbol?.fixed) ?? getPublicOrAbsoluteUrl('img/icons/marker/circle.svg');
|
||||
// double to match regularshapes using size as radius
|
||||
const uri = await getSVGUri(markerPath, config.size.fixed * 2);
|
||||
const style = config.style ?? defaultStyleConfig;
|
||||
const markerMaker = await getMarkerMaker(style.symbol?.fixed);
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
@ -113,32 +87,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
return; // ignore empty
|
||||
}
|
||||
|
||||
const makeIconStyle = (cfg: StyleMakerConfig) => {
|
||||
const icon = new style.Style({
|
||||
image: new style.Icon({
|
||||
src: uri,
|
||||
color: cfg.color,
|
||||
opacity: cfg.opacity,
|
||||
// scale based on field value
|
||||
scale: (DEFAULT_SIZE + cfg.size) / 100,
|
||||
}),
|
||||
});
|
||||
// transparent bounding box for featureAtPixel detection
|
||||
const boundingBox = new style.Style({
|
||||
image: new style.RegularShape({
|
||||
fill: new style.Fill({ color: 'rgba(0,0,0,0)' }),
|
||||
points: 4,
|
||||
radius: cfg.size,
|
||||
angle: Math.PI / 4,
|
||||
}),
|
||||
});
|
||||
return [icon, boundingBox]
|
||||
};
|
||||
|
||||
const marker = getMarkerFromPath(config.markerSymbol?.fixed);
|
||||
|
||||
const shape: StyleMaker = marker?.make ?? makeIconStyle;
|
||||
|
||||
const features: Feature<Point>[] = [];
|
||||
|
||||
for (const frame of data.series) {
|
||||
@ -148,15 +96,15 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
continue; // ???
|
||||
}
|
||||
|
||||
const colorDim = getColorDimension(frame, config.color, theme);
|
||||
const sizeDim = getScaledDimension(frame, config.size);
|
||||
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||
const colorDim = getColorDimension(frame, style.color ?? defaultStyleConfig.color, theme);
|
||||
const sizeDim = getScaledDimension(frame, style.size ?? defaultStyleConfig.size);
|
||||
const opacity = style?.opacity ?? defaultStyleConfig.opacity;
|
||||
|
||||
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
|
||||
colorDim: colorDim,
|
||||
sizeDim: sizeDim,
|
||||
opacity: opacity,
|
||||
styleMaker: shape,
|
||||
styleMaker: markerMaker,
|
||||
};
|
||||
|
||||
const frameFeatures = getFeatures(frame, info, featureDimensionConfig);
|
||||
@ -184,27 +132,22 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'config.size',
|
||||
path: 'config.size',
|
||||
id: 'config.style.size',
|
||||
path: 'config.style.size',
|
||||
name: 'Marker Size',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
min: 1,
|
||||
max: 100, // possible in the UI
|
||||
},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: DEFAULT_SIZE,
|
||||
min: 1,
|
||||
max: 20,
|
||||
},
|
||||
defaultValue: defaultOptions.style.size,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.markerSymbol',
|
||||
path: 'config.markerSymbol',
|
||||
id: 'config.style.symbol',
|
||||
path: 'config.style.symbol',
|
||||
name: 'Marker Symbol',
|
||||
editor: ResourceDimensionEditor,
|
||||
defaultValue: defaultOptions.markerSymbol,
|
||||
defaultValue: defaultOptions.style.symbol,
|
||||
settings: {
|
||||
resourceType: 'icon',
|
||||
showSourceRadio: false,
|
||||
@ -212,20 +155,17 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
id: 'config.style.color',
|
||||
path: 'config.style.color',
|
||||
name: 'Marker Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: 'grey',
|
||||
},
|
||||
defaultValue: defaultOptions.style.color,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.fillOpacity',
|
||||
path: 'config.style.opacity',
|
||||
name: 'Fill opacity',
|
||||
defaultValue: defaultOptions.fillOpacity,
|
||||
defaultValue: defaultOptions.style.opacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
|
@ -2,47 +2,43 @@ import { GrafanaTheme2, MapLayerOptions, MapLayerRegistryItem, PanelData, Plugin
|
||||
import Map from 'ol/Map';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
getColorDimension,
|
||||
getScaledDimension,
|
||||
getTextDimension,
|
||||
ScaleDimensionConfig,
|
||||
TextDimensionConfig,
|
||||
TextDimensionMode,
|
||||
} from 'app/features/dimensions';
|
||||
import { ColorDimensionEditor, ScaleDimensionEditor, TextDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { Fill, Stroke } from 'ol/style';
|
||||
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
|
||||
import { Feature } from 'ol';
|
||||
import { Point } from 'ol/geom';
|
||||
import { StyleMaker, StyleMakerConfig } from '../../types';
|
||||
import { textMarkerMaker } from '../../style/text';
|
||||
import { MarkersConfig } from './markersLayer';
|
||||
|
||||
interface TextLabelsConfig {
|
||||
labelText: TextDimensionConfig;
|
||||
color: ColorDimensionConfig;
|
||||
fillOpacity: number;
|
||||
fontSize: ScaleDimensionConfig;
|
||||
}
|
||||
|
||||
export const TEXT_LABELS_LAYER = 'text-labels';
|
||||
|
||||
const defaultOptions: TextLabelsConfig = {
|
||||
labelText: {
|
||||
// Same configuration
|
||||
type TextLabelsConfig = MarkersConfig;
|
||||
|
||||
const defaultOptions = {
|
||||
style: {
|
||||
text: {
|
||||
fixed: '',
|
||||
mode: TextDimensionMode.Field,
|
||||
},
|
||||
color: {
|
||||
fixed: 'dark-blue',
|
||||
},
|
||||
fillOpacity: 0.6,
|
||||
fontSize: {
|
||||
opacity: 1,
|
||||
size: {
|
||||
fixed: 10,
|
||||
min: 5,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
showLegend: false,
|
||||
};
|
||||
|
||||
export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
@ -62,23 +58,6 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
...options?.config,
|
||||
};
|
||||
|
||||
const fontFamily = theme.typography.fontFamily;
|
||||
|
||||
const getTextStyle = (text: string, fillColor: string, fontSize: number) => {
|
||||
return new style.Text({
|
||||
text: text,
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: fillColor }),
|
||||
font: `normal ${fontSize}px ${fontFamily}`,
|
||||
});
|
||||
};
|
||||
|
||||
const getStyle: StyleMaker = (cfg: StyleMakerConfig) => {
|
||||
return new style.Style({
|
||||
text: getTextStyle(cfg.text ?? defaultOptions.labelText.fixed, cfg.fillColor, cfg.size),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
@ -88,6 +67,9 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
|
||||
const features: Feature<Point>[] = [];
|
||||
|
||||
|
||||
const style = config.style ?? defaultOptions.style;
|
||||
|
||||
for (const frame of data.series) {
|
||||
const info = dataFrameToPoints(frame, matchers);
|
||||
if (info.warning) {
|
||||
@ -95,17 +77,17 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
return;
|
||||
}
|
||||
|
||||
const colorDim = getColorDimension(frame, config.color, theme);
|
||||
const textDim = getTextDimension(frame, config.labelText);
|
||||
const sizeDim = getScaledDimension(frame, config.fontSize);
|
||||
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||
const colorDim = getColorDimension(frame, style.color ?? defaultOptions.style.color, theme);
|
||||
const sizeDim = getScaledDimension(frame, style.size ?? defaultOptions.style.size);
|
||||
const opacity = style?.opacity ?? defaultOptions.style.opacity;
|
||||
const textDim = getTextDimension(frame, style.text ?? defaultOptions.style.text );
|
||||
|
||||
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
|
||||
colorDim: colorDim,
|
||||
sizeDim: sizeDim,
|
||||
textDim: textDim,
|
||||
opacity: opacity,
|
||||
styleMaker: getStyle,
|
||||
styleMaker: textMarkerMaker,
|
||||
};
|
||||
|
||||
const frameFeatures = getFeatures(frame, info, featureDimensionConfig);
|
||||
@ -122,22 +104,24 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'config.labelText',
|
||||
id: 'config.style.text',
|
||||
path: 'config.style.text',
|
||||
name: 'Text label',
|
||||
path: 'config.labelText',
|
||||
editor: TextDimensionEditor,
|
||||
defaultValue: defaultOptions.style.text,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
id: 'config.style.color',
|
||||
path: 'config.style.color',
|
||||
name: 'Text color',
|
||||
editor: ColorDimensionEditor,
|
||||
defaultValue: defaultOptions.style.color,
|
||||
settings: {},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.fillOpacity',
|
||||
path: 'config.style.opacity',
|
||||
name: 'Text opacity',
|
||||
defaultValue: defaultOptions.fillOpacity,
|
||||
defaultValue: defaultOptions.style.opacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
@ -145,14 +129,14 @@ export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.fontSize',
|
||||
path: 'config.fontSize',
|
||||
id: 'config.style.size',
|
||||
path: 'config.style.size',
|
||||
name: 'Text size',
|
||||
editor: ScaleDimensionEditor,
|
||||
defaultValue: defaultOptions.style.size,
|
||||
settings: {
|
||||
fixed: defaultOptions.fontSize.fixed,
|
||||
min: defaultOptions.fontSize.min,
|
||||
max: defaultOptions.fontSize.max,
|
||||
min: 2,
|
||||
max: 50,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -108,164 +108,64 @@ const simpleWorldmapConfig = {
|
||||
|
||||
describe('geomap migrations', () => {
|
||||
it('updates marker', () => {
|
||||
const panel = {
|
||||
id: 2,
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
const panel = ({
|
||||
type: 'geomap',
|
||||
title: 'Panel Title',
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
thresholds: {
|
||||
mode: 'absolute',
|
||||
steps: [
|
||||
{
|
||||
color: 'green',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
mappings: [],
|
||||
color: {
|
||||
mode: 'thresholds',
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
view: {
|
||||
id: 'zero',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
zoom: 1,
|
||||
},
|
||||
basemap: {
|
||||
type: 'default',
|
||||
config: {},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
type: 'markers',
|
||||
config: {
|
||||
color: {
|
||||
fixed: 'dark-green',
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
markerSymbol: {
|
||||
fixed: '',
|
||||
mode: 'fixed',
|
||||
},
|
||||
shape: 'circle',
|
||||
showLegend: true,
|
||||
size: {
|
||||
fixed: 5,
|
||||
max: 15,
|
||||
min: 2,
|
||||
max: 15,
|
||||
field: 'Count',
|
||||
},
|
||||
color: {
|
||||
fixed: 'dark-green',
|
||||
field: 'Price',
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
shape: 'triangle',
|
||||
showLegend: true,
|
||||
},
|
||||
location: {
|
||||
mode: 'auto',
|
||||
},
|
||||
type: 'markers',
|
||||
},
|
||||
],
|
||||
controls: {
|
||||
showZoom: true,
|
||||
mouseWheelZoom: true,
|
||||
showAttribution: true,
|
||||
showScale: false,
|
||||
showDebug: false,
|
||||
},
|
||||
},
|
||||
pluginVersion: '8.3.0-pre',
|
||||
datasource: null,
|
||||
} as PanelModel;
|
||||
pluginVersion: '8.2.0',
|
||||
} as any) as PanelModel;
|
||||
panel.options = mapMigrationHandler(panel);
|
||||
|
||||
expect(panel).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"datasource": null,
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"color": Object {
|
||||
"mode": "thresholds",
|
||||
},
|
||||
"mappings": Array [],
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 2,
|
||||
"options": Object {
|
||||
"basemap": Object {
|
||||
"config": Object {},
|
||||
"type": "default",
|
||||
},
|
||||
"controls": Object {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showScale": false,
|
||||
"showZoom": true,
|
||||
},
|
||||
"layers": Array [
|
||||
Object {
|
||||
"config": Object {
|
||||
"color": Object {
|
||||
"fixed": "dark-green",
|
||||
},
|
||||
"fillOpacity": 0.4,
|
||||
"markerSymbol": Object {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"showLegend": true,
|
||||
"size": Object {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2,
|
||||
"style": Object {
|
||||
"color": Object {
|
||||
"field": "Price",
|
||||
"fixed": "dark-green",
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"size": Object {
|
||||
"field": "Count",
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2,
|
||||
},
|
||||
"symbol": Object {
|
||||
"fixed": "img/icons/marker/triangle.svg",
|
||||
"mode": "fixed",
|
||||
},
|
||||
},
|
||||
},
|
||||
"location": Object {
|
||||
"mode": "auto",
|
||||
},
|
||||
"type": "markers",
|
||||
},
|
||||
],
|
||||
"view": Object {
|
||||
"id": "zero",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 1,
|
||||
},
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"title": "Panel Title",
|
||||
"pluginVersion": "8.2.0",
|
||||
"type": "geomap",
|
||||
}
|
||||
`);
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { FieldConfigSource, PanelModel, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
|
||||
import { ResourceDimensionMode } from 'app/features/dimensions';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { MarkersConfig } from './layers/data/markersLayer';
|
||||
import { getMarkerAsPath } from './style/markers';
|
||||
import { defaultStyleConfig } from './style/types';
|
||||
import { GeomapPanelOptions } from './types';
|
||||
import { markerMakers } from './utils/regularShapes';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
/**
|
||||
@ -100,23 +104,38 @@ function asNumber(v: any): number | undefined {
|
||||
}
|
||||
|
||||
export const mapMigrationHandler = (panel: PanelModel): Partial<GeomapPanelOptions> => {
|
||||
const pluginVersion = panel?.pluginVersion;
|
||||
if (pluginVersion?.startsWith('8.1') || pluginVersion?.startsWith('8.2') || pluginVersion?.startsWith('8.3')) {
|
||||
if (panel.options?.layers?.length > 0) {
|
||||
const pluginVersion = panel?.pluginVersion ?? '';
|
||||
|
||||
// before 8.3, only one layer was supported!
|
||||
if (pluginVersion.startsWith('8.1') || pluginVersion.startsWith('8.2')) {
|
||||
const layers = panel.options?.layers;
|
||||
if (layers?.length === 1) {
|
||||
const layer = panel.options.layers[0];
|
||||
if (layer?.type === 'markers') {
|
||||
const shape = layer?.config?.shape;
|
||||
if (shape) {
|
||||
const marker = markerMakers.getIfExists(shape);
|
||||
if (marker?.aliasIds && marker.aliasIds?.length > 0) {
|
||||
layer.config.markerSymbol = {
|
||||
fixed: marker.aliasIds[0],
|
||||
mode: 'fixed',
|
||||
};
|
||||
delete layer.config.shape;
|
||||
}
|
||||
return { ...panel.options, layers: Object.assign([], ...panel.options.layers, { 0: layer }) };
|
||||
if (layer?.type === 'markers' && layer.config) {
|
||||
// Moving style to child object
|
||||
const oldConfig = layer.config;
|
||||
const config: MarkersConfig = {
|
||||
style: cloneDeep(defaultStyleConfig),
|
||||
showLegend: Boolean(oldConfig.showLegend),
|
||||
};
|
||||
|
||||
if (oldConfig.size) {
|
||||
config.style.size = oldConfig.size;
|
||||
}
|
||||
if (oldConfig.color) {
|
||||
config.style.color = oldConfig.color;
|
||||
}
|
||||
if (oldConfig.fillOpacity) {
|
||||
config.style.opacity = oldConfig.fillOpacity;
|
||||
}
|
||||
const symbol = getMarkerAsPath(oldConfig.shape);
|
||||
if (symbol) {
|
||||
config.style.symbol = {
|
||||
fixed: symbol,
|
||||
mode: ResourceDimensionMode.Fixed,
|
||||
};
|
||||
}
|
||||
return { ...panel.options, layers: [{ ...layer, config }] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
257
public/app/plugins/panel/geomap/style/markers.ts
Normal file
257
public/app/plugins/panel/geomap/style/markers.ts
Normal file
@ -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;
|
||||
}
|
16
public/app/plugins/panel/geomap/style/text.ts
Normal file
16
public/app/plugins/panel/geomap/style/text.ts
Normal file
@ -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}`,
|
||||
}),
|
||||
});
|
||||
};
|
81
public/app/plugins/panel/geomap/style/types.ts
Normal file
81
public/app/plugins/panel/geomap/style/types.ts
Normal file
@ -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[];
|
18
public/app/plugins/panel/geomap/style/utils.ts
Normal file
18
public/app/plugins/panel/geomap/style/utils.ts
Normal file
@ -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,7 +1,6 @@
|
||||
import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { Units } from 'ol/proj/Units';
|
||||
import { Style } from 'ol/style';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
export interface ControlsOptions {
|
||||
@ -64,10 +63,10 @@ export enum ComparisonOperation {
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
}
|
||||
|
||||
export interface GazetteerPathEditorConfigSettings {
|
||||
options?: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
//-------------------
|
||||
// Runtime model
|
||||
//-------------------
|
||||
@ -79,13 +78,3 @@ export interface MapLayerState<TConfig = any> {
|
||||
onChange: (cfg: MapLayerOptions<TConfig>) => void;
|
||||
isBasemap?: boolean;
|
||||
}
|
||||
export interface StyleMakerConfig {
|
||||
color: string;
|
||||
fillColor: string;
|
||||
size: number;
|
||||
markerPath?: string;
|
||||
text?: string;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export type StyleMaker = (config: StyleMakerConfig) => Style | Style[];
|
||||
|
@ -2,8 +2,7 @@ import { DataFrame } from '@grafana/data';
|
||||
import { DimensionSupplier } from 'app/features/dimensions';
|
||||
import { Feature } from 'ol';
|
||||
import { Point } from 'ol/geom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { StyleMaker } from '../types';
|
||||
import { StyleMaker } from '../style/types';
|
||||
import { LocationInfo } from './location';
|
||||
|
||||
export interface FeaturesStylesBuilderConfig {
|
||||
@ -20,6 +19,7 @@ export const getFeatures = (
|
||||
config: FeaturesStylesBuilderConfig
|
||||
): Array<Feature<Point>> | undefined => {
|
||||
const features: Array<Feature<Point>> = [];
|
||||
const opacity = config.opacity;
|
||||
|
||||
// Map each data value into new points
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
@ -30,10 +30,7 @@ export const getFeatures = (
|
||||
const size = config.sizeDim.get(i);
|
||||
|
||||
// Get the text for the feature based on text dimension
|
||||
const label = config?.textDim ? config?.textDim.get(i) : undefined;
|
||||
|
||||
// Set the opacity determined from user configuration
|
||||
const fillColor = tinycolor(color).setAlpha(config?.opacity).toRgbString();
|
||||
const text = config?.textDim ? config?.textDim.get(i) : undefined;
|
||||
|
||||
// Create a new Feature for each point returned from dataFrameToPoints
|
||||
const dot = new Feature(info.points[i]);
|
||||
@ -42,11 +39,7 @@ export const getFeatures = (
|
||||
rowIndex: i,
|
||||
});
|
||||
|
||||
if (config?.textDim) {
|
||||
dot.setStyle(config.styleMaker({ color, fillColor, size, text: label }));
|
||||
} else {
|
||||
dot.setStyle(config.styleMaker({ color, fillColor, size }));
|
||||
}
|
||||
dot.setStyle(config.styleMaker({ color, size, text, opacity }));
|
||||
features.push(dot);
|
||||
}
|
||||
|
||||
|
@ -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
Block a user