[v8.3.x] Backport Latest Geomap Commits (#42399)

This commit is contained in:
Nathan Marrs 2021-11-29 11:26:44 -08:00 committed by GitHub
parent d246e600b8
commit 30933fbb3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 891 additions and 61 deletions

View 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": ""
}

View File

@ -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]);
}

View File

@ -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>
);
}
}

View 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;
`,
});

View File

@ -2,4 +2,5 @@ export * from './ColorDimensionEditor';
export * from './IconSelector';
export * from './ResourceDimensionEditor';
export * from './ScaleDimensionEditor';
export * from './ScalarDimensionEditor';
export * from './TextDimensionEditor';

View File

@ -6,3 +6,4 @@ export * from './text';
export * from './utils';
export * from './resource';
export * from './context';
export * from './scalar';

View 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]);
});
});

View 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),
};
}

View File

@ -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?
}

View File

@ -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 },

View File

@ -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({

View File

@ -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,

View File

@ -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,
}),
}),
];

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}