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