mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[v8.3.x] Backport Latest Geomap Commits (#42399)
This commit is contained in:
parent
d246e600b8
commit
30933fbb3a
436
devenv/dev-dashboards/panel-geomap/geomap_multi-layers.json
Normal file
436
devenv/dev-dashboards/panel-geomap/geomap_multi-layers.json
Normal file
@ -0,0 +1,436 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 157,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 22,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"rules": [
|
||||
{
|
||||
"check": {
|
||||
"operation": "eq",
|
||||
"property": "name",
|
||||
"value": "Greenland"
|
||||
},
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "light-blue"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"operation": "eq",
|
||||
"property": "name",
|
||||
"value": "Antarctica"
|
||||
},
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "#FCE2DE"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"operation": "eq",
|
||||
"property": "name",
|
||||
"value": "Canada"
|
||||
},
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "#37872D"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"operation": "eq",
|
||||
"property": "name",
|
||||
"value": "Mexico"
|
||||
},
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "#1F60C4"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"src": "public/maps/countries.geojson",
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.1,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Countries",
|
||||
"type": "geojson"
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-blue"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/square.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"text": {
|
||||
"field": "Count",
|
||||
"fixed": "",
|
||||
"mode": "field"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 16,
|
||||
"offsetX": 10,
|
||||
"offsetY": 10,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"mode": "auto"
|
||||
},
|
||||
"name": "Flights",
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 42.826559,
|
||||
"lon": -96.868893,
|
||||
"zoom": 3.58
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Multi layers",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 22,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.7,
|
||||
"rotation": {
|
||||
"field": "Lng",
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 10,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/plane.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"text": {
|
||||
"field": "Lng",
|
||||
"fixed": "",
|
||||
"mode": "field"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 18,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Layer 2",
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 42.826559,
|
||||
"lon": -96.868893,
|
||||
"zoom": 3.58
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Markers",
|
||||
"type": "geomap"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 33,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"geomap",
|
||||
"panel-tests"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Geomap Multi Layers",
|
||||
"uid": "2jFpEvp7z",
|
||||
"version": 8,
|
||||
"weekStart": ""
|
||||
}
|
@ -68,7 +68,8 @@ export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field)
|
||||
export function useSelectOptions(
|
||||
displayNames: FrameFieldsDisplayNames,
|
||||
currentName?: string,
|
||||
firstItem?: SelectableValue<string>
|
||||
firstItem?: SelectableValue<string>,
|
||||
fieldType?: string
|
||||
): Array<SelectableValue<string>> {
|
||||
return useMemo(() => {
|
||||
let found = false;
|
||||
@ -81,11 +82,13 @@ export function useSelectOptions(
|
||||
found = true;
|
||||
}
|
||||
const field = displayNames.fields.get(name);
|
||||
options.push({
|
||||
value: name,
|
||||
label: name,
|
||||
icon: field ? getFieldTypeIcon(field) : undefined,
|
||||
});
|
||||
if (!fieldType || fieldType === field?.type) {
|
||||
options.push({
|
||||
value: name,
|
||||
label: name,
|
||||
icon: field ? getFieldTypeIcon(field) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const name of displayNames.raw) {
|
||||
if (!displayNames.display.has(name)) {
|
||||
@ -106,5 +109,5 @@ export function useSelectOptions(
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}, [displayNames, currentName, firstItem]);
|
||||
}, [displayNames, currentName, firstItem, fieldType]);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Input } from '@grafana/ui';
|
||||
import { Field, Input } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
value?: number;
|
||||
@ -13,16 +13,18 @@ interface Props {
|
||||
|
||||
interface State {
|
||||
text: string;
|
||||
inputCorrected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an Input field that will call `onChange` for blur and enter
|
||||
*/
|
||||
export class NumberInput extends PureComponent<Props, State> {
|
||||
state: State = { text: '' };
|
||||
state: State = { text: '', inputCorrected: false };
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||
});
|
||||
}
|
||||
@ -30,6 +32,7 @@ export class NumberInput extends PureComponent<Props, State> {
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (this.props.value !== oldProps.value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||
});
|
||||
}
|
||||
@ -42,11 +45,30 @@ export class NumberInput extends PureComponent<Props, State> {
|
||||
value = e.currentTarget.valueAsNumber;
|
||||
}
|
||||
this.props.onChange(value);
|
||||
this.setState({ ...this.state, inputCorrected: false });
|
||||
};
|
||||
|
||||
onChange = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
let newValue: string | undefined = undefined;
|
||||
let corrected = false;
|
||||
const min = this.props.min;
|
||||
const max = this.props.max;
|
||||
const currValue = e.currentTarget.valueAsNumber;
|
||||
if (!Number.isNaN(currValue)) {
|
||||
if (min != null && currValue < min) {
|
||||
newValue = min.toString();
|
||||
corrected = true;
|
||||
} else if (max != null && currValue > max) {
|
||||
newValue = max.toString();
|
||||
corrected = true;
|
||||
} else {
|
||||
newValue = e.currentTarget.value;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
text: e.currentTarget.value,
|
||||
...this.state,
|
||||
text: newValue ? newValue : '',
|
||||
inputCorrected: corrected,
|
||||
});
|
||||
};
|
||||
|
||||
@ -58,20 +80,22 @@ export class NumberInput extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { placeholder } = this.props;
|
||||
const { text } = this.state;
|
||||
const { text, inputCorrected } = this.state;
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={this.props.step}
|
||||
autoFocus={this.props.autoFocus}
|
||||
value={text}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onKeyPress={this.onKeyPress}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Field invalid={inputCorrected} error={inputCorrected ? 'Cannot go beyond range' : ''}>
|
||||
<Input
|
||||
type="number"
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={this.props.step}
|
||||
autoFocus={this.props.autoFocus}
|
||||
value={text}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onKeyPress={this.onKeyPress}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
116
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
116
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { ScalarDimensionConfig, ScalarDimensionMode, ScalarDimensionOptions } from '../types';
|
||||
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, useStyles2 } from '@grafana/ui';
|
||||
import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/src/components/MatchersUI/utils';
|
||||
import { NumberInput } from './NumberInput';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
const fixedValueOption: SelectableValue<string> = {
|
||||
label: 'Fixed value',
|
||||
value: '_____fixed_____',
|
||||
};
|
||||
|
||||
const scalarOptions = [
|
||||
{ label: 'Mod', value: ScalarDimensionMode.Mod, description: 'Use field values, mod from max' },
|
||||
{ label: 'Clamped', value: ScalarDimensionMode.Clamped, description: 'Use field values, clamped to max and min' },
|
||||
];
|
||||
|
||||
export const ScalarDimensionEditor: FC<StandardEditorProps<ScalarDimensionConfig, ScalarDimensionOptions, any>> = (
|
||||
props
|
||||
) => {
|
||||
const { value, context, onChange, item } = props;
|
||||
const { settings } = item;
|
||||
|
||||
const DEFAULT_VALUE = 0;
|
||||
|
||||
const fieldName = value?.field;
|
||||
const isFixed = Boolean(!fieldName);
|
||||
const names = useFieldDisplayNames(context.data);
|
||||
const selectOptions = useSelectOptions(names, fieldName, fixedValueOption, FieldType.number);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(selection: SelectableValue<string>) => {
|
||||
const field = selection.value;
|
||||
if (field && field !== fixedValueOption.value) {
|
||||
onChange({
|
||||
...value,
|
||||
field,
|
||||
});
|
||||
} else {
|
||||
const fixed = value.fixed ?? DEFAULT_VALUE;
|
||||
onChange({
|
||||
...value,
|
||||
field: undefined,
|
||||
fixed,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const onModeChange = useCallback(
|
||||
(mode) => {
|
||||
onChange({
|
||||
...value,
|
||||
mode,
|
||||
});
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(v: number | undefined) => {
|
||||
onChange({
|
||||
...value,
|
||||
field: undefined,
|
||||
fixed: v ?? DEFAULT_VALUE,
|
||||
});
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const val = value ?? {};
|
||||
const mode = value?.mode ?? ScalarDimensionMode.Mod;
|
||||
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Limit" labelWidth={8} grow={true}>
|
||||
<RadioButtonGroup value={mode} options={scalarOptions} onChange={onModeChange} fullWidth />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={onSelectChange}
|
||||
noOptionsMessage="No fields found"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.range}>
|
||||
{isFixed && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Value" labelWidth={8} grow={true}>
|
||||
<NumberInput
|
||||
value={val?.fixed ?? DEFAULT_VALUE}
|
||||
onChange={onValueChange}
|
||||
max={settings?.max}
|
||||
min={settings?.min}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
range: css`
|
||||
padding-top: 8px;
|
||||
`,
|
||||
});
|
@ -2,4 +2,5 @@ export * from './ColorDimensionEditor';
|
||||
export * from './IconSelector';
|
||||
export * from './ResourceDimensionEditor';
|
||||
export * from './ScaleDimensionEditor';
|
||||
export * from './ScalarDimensionEditor';
|
||||
export * from './TextDimensionEditor';
|
||||
|
@ -6,3 +6,4 @@ export * from './text';
|
||||
export * from './utils';
|
||||
export * from './resource';
|
||||
export * from './context';
|
||||
export * from './scalar';
|
||||
|
94
public/app/features/dimensions/scalar.test.ts
Normal file
94
public/app/features/dimensions/scalar.test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
|
||||
import { ScalarDimensionMode } from '.';
|
||||
import { getScalarDimension } from './scalar';
|
||||
|
||||
describe('scalar dimensions', () => {
|
||||
it('handles string field', () => {
|
||||
const values = ['-720', '10', '540', '90', '-210'];
|
||||
const frame: DataFrame = {
|
||||
name: 'a',
|
||||
length: values.length,
|
||||
fields: [
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(values),
|
||||
config: {
|
||||
min: -720,
|
||||
max: 540,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const supplier = getScalarDimension(frame, {
|
||||
min: -360,
|
||||
max: 360,
|
||||
field: 'test',
|
||||
fixed: 0,
|
||||
mode: ScalarDimensionMode.Clamped,
|
||||
});
|
||||
|
||||
const clamped = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||
expect(clamped).toEqual([0, 0, 0, 0, 0]);
|
||||
});
|
||||
it('clamps out of range values', () => {
|
||||
const values = [-720, 10, 540, 90, -210];
|
||||
const frame: DataFrame = {
|
||||
name: 'a',
|
||||
length: values.length,
|
||||
fields: [
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(values),
|
||||
config: {
|
||||
min: -720,
|
||||
max: 540,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const supplier = getScalarDimension(frame, {
|
||||
min: -360,
|
||||
max: 360,
|
||||
field: 'test',
|
||||
fixed: 0,
|
||||
mode: ScalarDimensionMode.Clamped,
|
||||
});
|
||||
|
||||
const clamped = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||
expect(clamped).toEqual([-360, 10, 360, 90, -210]);
|
||||
});
|
||||
|
||||
it('keeps remainder after divisible by max', () => {
|
||||
const values = [-721, 10, 540, 390, -210];
|
||||
const frame: DataFrame = {
|
||||
name: 'a',
|
||||
length: values.length,
|
||||
fields: [
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(values),
|
||||
config: {
|
||||
min: -721,
|
||||
max: 540,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const supplier = getScalarDimension(frame, {
|
||||
min: -360,
|
||||
max: 360,
|
||||
field: 'test',
|
||||
fixed: 0,
|
||||
mode: ScalarDimensionMode.Mod,
|
||||
});
|
||||
|
||||
const remainder = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||
expect(remainder).toEqual([-1, 10, 180, 30, -210]);
|
||||
});
|
||||
});
|
59
public/app/features/dimensions/scalar.ts
Normal file
59
public/app/features/dimensions/scalar.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
import { DimensionSupplier, ScalarDimensionConfig, ScalarDimensionMode } from './types';
|
||||
import { findField, getLastNotNullFieldValue } from './utils';
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Scalar dimension
|
||||
//---------------------------------------------------------
|
||||
export function getScalarDimension(
|
||||
frame: DataFrame | undefined,
|
||||
config: ScalarDimensionConfig
|
||||
): DimensionSupplier<number> {
|
||||
return getScalarDimensionForField(findField(frame, config?.field), config);
|
||||
}
|
||||
export function getScalarDimensionForField(
|
||||
field: Field | undefined,
|
||||
cfg: ScalarDimensionConfig
|
||||
): DimensionSupplier<number> {
|
||||
if (!field) {
|
||||
const v = cfg.fixed ?? 0;
|
||||
return {
|
||||
isAssumed: Boolean(cfg.field?.length) || !cfg.fixed,
|
||||
fixed: v,
|
||||
value: () => v,
|
||||
get: () => v,
|
||||
};
|
||||
}
|
||||
|
||||
//mod mode as default
|
||||
let validated = (value: number) => {
|
||||
return value % cfg.max;
|
||||
};
|
||||
|
||||
//capped mode
|
||||
if (cfg.mode === ScalarDimensionMode.Clamped) {
|
||||
validated = (value: number) => {
|
||||
if (value < cfg.min) {
|
||||
return cfg.min;
|
||||
}
|
||||
if (value > cfg.max) {
|
||||
return cfg.max;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
const get = (i: number) => {
|
||||
const v = field.values.get(i);
|
||||
if (v === null || typeof v !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
return validated(v);
|
||||
};
|
||||
|
||||
return {
|
||||
field,
|
||||
get,
|
||||
value: () => getLastNotNullFieldValue(field),
|
||||
};
|
||||
}
|
@ -51,6 +51,21 @@ export interface ScaleDimensionOptions {
|
||||
hideRange?: boolean; // false
|
||||
}
|
||||
|
||||
export enum ScalarDimensionMode {
|
||||
Mod = 'mod',
|
||||
Clamped = 'clamped',
|
||||
}
|
||||
export interface ScalarDimensionConfig extends BaseDimensionConfig<number> {
|
||||
mode: ScalarDimensionMode;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface ScalarDimensionOptions {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface TextDimensionOptions {
|
||||
// anything?
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
ColorDimensionEditor,
|
||||
ResourceDimensionEditor,
|
||||
ScaleDimensionEditor,
|
||||
ScalarDimensionEditor,
|
||||
TextDimensionEditor,
|
||||
} from 'app/features/dimensions/editors';
|
||||
import {
|
||||
@ -27,14 +28,16 @@ import {
|
||||
ResourceFolderName,
|
||||
TextDimensionConfig,
|
||||
defaultTextConfig,
|
||||
ScalarDimensionConfig,
|
||||
} from 'app/features/dimensions/types';
|
||||
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||
import { defaultStyleConfig, GeometryTypeId, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||
import { styleUsesText } from '../../style/utils';
|
||||
import { LayerContentInfo } from '../../utils/getFeatures';
|
||||
|
||||
export interface StyleEditorOptions {
|
||||
layerInfo?: Observable<LayerContentInfo>;
|
||||
simpleFixedValues?: boolean;
|
||||
displayRotation?: boolean;
|
||||
}
|
||||
|
||||
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
||||
@ -43,6 +46,8 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
onChange,
|
||||
item,
|
||||
}) => {
|
||||
const settings = item.settings;
|
||||
|
||||
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
||||
onChange({ ...value, size: sizeValue });
|
||||
};
|
||||
@ -59,6 +64,10 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
onChange({ ...value, opacity: opacityValue });
|
||||
};
|
||||
|
||||
const onRotationChange = (rotationValue: ScalarDimensionConfig | undefined) => {
|
||||
onChange({ ...value, rotation: rotationValue });
|
||||
};
|
||||
|
||||
const onTextChange = (textValue: TextDimensionConfig | undefined) => {
|
||||
onChange({ ...value, text: textValue });
|
||||
};
|
||||
@ -84,44 +93,60 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
};
|
||||
|
||||
let featuresHavePoints = false;
|
||||
if (item.settings?.layerInfo) {
|
||||
const propertyOptions = useObservable(item.settings?.layerInfo);
|
||||
featuresHavePoints = propertyOptions?.geometryType === 'point';
|
||||
if (settings?.layerInfo) {
|
||||
const propertyOptions = useObservable(settings?.layerInfo);
|
||||
featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
|
||||
}
|
||||
|
||||
const hasTextLabel = styleUsesText(value);
|
||||
|
||||
// Simple fixed value display
|
||||
if (item.settings?.simpleFixedValues) {
|
||||
if (settings?.simpleFixedValues) {
|
||||
return (
|
||||
<>
|
||||
{featuresHavePoints && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value.symbol ?? defaultStyleConfig.symbol}
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: 'icon',
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<Field label={'Rotation angle'}>
|
||||
<ScalarDimensionEditor
|
||||
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
onChange={onRotationChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: 'icon',
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
min: defaultStyleConfig.rotation.min,
|
||||
max: defaultStyleConfig.rotation.max,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Color" labelWidth={10}>
|
||||
<InlineLabel width={4}>
|
||||
<ColorPicker
|
||||
color={value.color?.fixed ?? defaultStyleConfig.color.fixed}
|
||||
color={value?.color?.fixed ?? defaultStyleConfig.color.fixed}
|
||||
onChange={(v) => {
|
||||
onColorChange({ fixed: v });
|
||||
}}
|
||||
@ -132,7 +157,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Opacity" labelWidth={10} grow>
|
||||
<SliderValueEditor
|
||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
||||
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||
context={context}
|
||||
onChange={onOpacityChange}
|
||||
item={
|
||||
@ -155,7 +180,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
<>
|
||||
<Field label={'Size'}>
|
||||
<ScaleDimensionEditor
|
||||
value={value.size ?? defaultStyleConfig.size}
|
||||
value={value?.size ?? defaultStyleConfig.size}
|
||||
context={context}
|
||||
onChange={onSizeChange}
|
||||
item={
|
||||
@ -170,7 +195,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value.symbol ?? defaultStyleConfig.symbol}
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
@ -188,7 +213,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'Color'}>
|
||||
<ColorDimensionEditor
|
||||
value={value.color ?? defaultStyleConfig.color}
|
||||
value={value?.color ?? defaultStyleConfig.color}
|
||||
context={context}
|
||||
onChange={onColorChange}
|
||||
item={{} as any}
|
||||
@ -196,7 +221,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'Fill opacity'}>
|
||||
<SliderValueEditor
|
||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
||||
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||
context={context}
|
||||
onChange={onOpacityChange}
|
||||
item={
|
||||
@ -210,9 +235,26 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{settings?.displayRotation && (
|
||||
<Field label={'Rotation angle'}>
|
||||
<ScalarDimensionEditor
|
||||
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||
context={context}
|
||||
onChange={onRotationChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
min: defaultStyleConfig.rotation.min,
|
||||
max: defaultStyleConfig.rotation.max,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field label={'Text label'}>
|
||||
<TextDimensionEditor
|
||||
value={value.text ?? defaultTextConfig}
|
||||
value={value?.text ?? defaultTextConfig}
|
||||
context={context}
|
||||
onChange={onTextChange}
|
||||
item={{} as any}
|
||||
@ -224,7 +266,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
<HorizontalGroup>
|
||||
<Field label={'Font size'}>
|
||||
<NumberValueEditor
|
||||
value={value.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
||||
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
||||
context={context}
|
||||
onChange={onTextFontSizeChange}
|
||||
item={{} as any}
|
||||
@ -232,7 +274,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'X offset'}>
|
||||
<NumberValueEditor
|
||||
value={value.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
||||
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
||||
context={context}
|
||||
onChange={onTextOffsetXChange}
|
||||
item={{} as any}
|
||||
@ -240,7 +282,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'Y offset'}>
|
||||
<NumberValueEditor
|
||||
value={value.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
||||
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
||||
context={context}
|
||||
onChange={onTextOffsetYChange}
|
||||
item={{} as any}
|
||||
@ -249,7 +291,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</HorizontalGroup>
|
||||
<Field label={'Align'}>
|
||||
<RadioButtonGroup
|
||||
value={value.textConfig?.textAlign ?? defaultStyleConfig.textConfig.textAlign}
|
||||
value={value?.textConfig?.textAlign ?? defaultStyleConfig.textConfig.textAlign}
|
||||
onChange={onTextAlignChange}
|
||||
options={[
|
||||
{ value: TextAlignment.Left, label: TextAlignment.Left },
|
||||
@ -260,7 +302,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</Field>
|
||||
<Field label={'Baseline'}>
|
||||
<RadioButtonGroup
|
||||
value={value.textConfig?.textBaseline ?? defaultStyleConfig.textConfig.textBaseline}
|
||||
value={value?.textConfig?.textBaseline ?? defaultStyleConfig.textConfig.textBaseline}
|
||||
onChange={onTextBaselineChange}
|
||||
options={[
|
||||
{ value: TextBaseline.Top, label: TextBaseline.Top },
|
||||
|
@ -12,7 +12,7 @@ import { Point } from 'ol/geom';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import { getScaledDimension, getColorDimension, getTextDimension } from 'app/features/dimensions';
|
||||
import { getScaledDimension, getColorDimension, getTextDimension, getScalarDimension } from 'app/features/dimensions';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
@ -107,6 +107,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
if (style.fields.text) {
|
||||
dims.text = getTextDimension(frame, style.config.text!);
|
||||
}
|
||||
if (style.fields.rotation) {
|
||||
dims.rotation = getScalarDimension(frame, style.config.rotation ?? defaultStyleConfig.rotation);
|
||||
}
|
||||
style.dims = dims;
|
||||
}
|
||||
|
||||
@ -139,7 +142,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
path: 'config.style',
|
||||
name: 'Styles',
|
||||
editor: StyleEditor,
|
||||
settings: {},
|
||||
settings: {
|
||||
displayRotation: true,
|
||||
},
|
||||
defaultValue: defaultOptions.style,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
|
@ -150,6 +150,12 @@ describe('geomap migrations', () => {
|
||||
"fixed": "dark-green",
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": Object {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod",
|
||||
},
|
||||
"size": Object {
|
||||
"field": "Count",
|
||||
"fixed": 5,
|
||||
|
@ -121,13 +121,14 @@ const makers: SymbolMaker[] = [
|
||||
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,
|
||||
angle: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@ -139,13 +140,14 @@ const makers: SymbolMaker[] = [
|
||||
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: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
angle: 0,
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
@ -158,6 +160,7 @@ const makers: SymbolMaker[] = [
|
||||
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 }),
|
||||
@ -166,6 +169,7 @@ const makers: SymbolMaker[] = [
|
||||
radius,
|
||||
radius2: radius * 0.4,
|
||||
angle: 0,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@ -177,6 +181,7 @@ const makers: SymbolMaker[] = [
|
||||
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 }),
|
||||
@ -184,6 +189,7 @@ const makers: SymbolMaker[] = [
|
||||
radius,
|
||||
radius2: 0,
|
||||
angle: 0,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@ -195,13 +201,14 @@ const makers: SymbolMaker[] = [
|
||||
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,
|
||||
angle: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@ -265,6 +272,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
||||
make: src
|
||||
? (cfg: StyleConfigValues) => {
|
||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||
const rotation = cfg.rotation ?? 0;
|
||||
return [
|
||||
new Style({
|
||||
image: new Icon({
|
||||
@ -272,6 +280,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
||||
color: cfg.color,
|
||||
opacity: cfg.opacity ?? 1,
|
||||
scale: (DEFAULT_SIZE + radius) / 100,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
}),
|
||||
text: !cfg?.text ? undefined : textLabel(cfg),
|
||||
}),
|
||||
@ -281,7 +290,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
||||
fill: new Fill({ color: 'rgba(0,0,0,0)' }),
|
||||
points: 4,
|
||||
radius: cfg.size,
|
||||
angle: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
ResourceDimensionConfig,
|
||||
ResourceDimensionMode,
|
||||
ScaleDimensionConfig,
|
||||
ScalarDimensionConfig,
|
||||
ScalarDimensionMode,
|
||||
TextDimensionConfig,
|
||||
} from 'app/features/dimensions';
|
||||
import { Style } from 'ol/style';
|
||||
@ -30,6 +32,9 @@ export interface StyleConfig {
|
||||
// Can show markers and text together!
|
||||
text?: TextDimensionConfig;
|
||||
textConfig?: TextStyleConfig;
|
||||
|
||||
// Allow for rotation of markers
|
||||
rotation?: ScalarDimensionConfig;
|
||||
}
|
||||
|
||||
export const DEFAULT_SIZE = 5;
|
||||
@ -66,6 +71,12 @@ export const defaultStyleConfig = Object.freeze({
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
rotation: {
|
||||
fixed: 0,
|
||||
mode: ScalarDimensionMode.Mod,
|
||||
min: -360,
|
||||
max: 360,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@ -99,12 +110,14 @@ export interface StyleConfigFields {
|
||||
color?: string;
|
||||
size?: string;
|
||||
text?: string;
|
||||
rotation?: string;
|
||||
}
|
||||
|
||||
export interface StyleDimensions {
|
||||
color?: DimensionSupplier<string>;
|
||||
size?: DimensionSupplier<number>;
|
||||
text?: DimensionSupplier<string>;
|
||||
rotation?: DimensionSupplier<number>;
|
||||
}
|
||||
|
||||
export interface StyleConfigState {
|
||||
|
@ -5,7 +5,7 @@ import { defaultStyleConfig, StyleConfig, StyleConfigFields, StyleConfigState }
|
||||
|
||||
/** Indicate if the style wants to show text values */
|
||||
export function styleUsesText(config: StyleConfig): boolean {
|
||||
const { text } = config;
|
||||
const text = config?.text;
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
@ -35,7 +35,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
||||
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
||||
lineWidth: cfg.lineWidth ?? 1,
|
||||
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
||||
rotation: 0, // dynamic will follow path
|
||||
rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later
|
||||
},
|
||||
maker,
|
||||
};
|
||||
@ -46,6 +46,9 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
||||
if (cfg.size?.field?.length) {
|
||||
fields.size = cfg.size.field;
|
||||
}
|
||||
if (cfg.rotation?.field?.length) {
|
||||
fields.rotation = cfg.rotation.field;
|
||||
}
|
||||
|
||||
if (hasText) {
|
||||
state.base.text = cfg.text?.fixed;
|
||||
|
@ -31,6 +31,9 @@ export const getFeatures = (
|
||||
if (dims.size) {
|
||||
values.size = dims.size.get(i);
|
||||
}
|
||||
if (dims.rotation) {
|
||||
values.rotation = dims.rotation.get(i);
|
||||
}
|
||||
if (dims.text) {
|
||||
values.text = dims.text.get(i);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user