mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Add rotation option for markers layer (#41992)
* add rotation option for markers layer * add ScalerDimensionEditor
This commit is contained in:
parent
0921037f32
commit
92e1883845
@ -68,7 +68,8 @@ export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field)
|
|||||||
export function useSelectOptions(
|
export function useSelectOptions(
|
||||||
displayNames: FrameFieldsDisplayNames,
|
displayNames: FrameFieldsDisplayNames,
|
||||||
currentName?: string,
|
currentName?: string,
|
||||||
firstItem?: SelectableValue<string>
|
firstItem?: SelectableValue<string>,
|
||||||
|
fieldType?: string
|
||||||
): Array<SelectableValue<string>> {
|
): Array<SelectableValue<string>> {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
let found = false;
|
let found = false;
|
||||||
@ -81,11 +82,13 @@ export function useSelectOptions(
|
|||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
const field = displayNames.fields.get(name);
|
const field = displayNames.fields.get(name);
|
||||||
options.push({
|
if (!fieldType || fieldType === field?.type) {
|
||||||
value: name,
|
options.push({
|
||||||
label: name,
|
value: name,
|
||||||
icon: field ? getFieldTypeIcon(field) : undefined,
|
label: name,
|
||||||
});
|
icon: field ? getFieldTypeIcon(field) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const name of displayNames.raw) {
|
for (const name of displayNames.raw) {
|
||||||
if (!displayNames.display.has(name)) {
|
if (!displayNames.display.has(name)) {
|
||||||
@ -106,5 +109,5 @@ export function useSelectOptions(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}, [displayNames, currentName, firstItem]);
|
}, [displayNames, currentName, firstItem, fieldType]);
|
||||||
}
|
}
|
||||||
|
110
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
110
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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 } = props;
|
||||||
|
|
||||||
|
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} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
range: css`
|
||||||
|
padding-top: 8px;
|
||||||
|
`,
|
||||||
|
});
|
@ -2,4 +2,5 @@ export * from './ColorDimensionEditor';
|
|||||||
export * from './IconSelector';
|
export * from './IconSelector';
|
||||||
export * from './ResourceDimensionEditor';
|
export * from './ResourceDimensionEditor';
|
||||||
export * from './ScaleDimensionEditor';
|
export * from './ScaleDimensionEditor';
|
||||||
|
export * from './ScalarDimensionEditor';
|
||||||
export * from './TextDimensionEditor';
|
export * from './TextDimensionEditor';
|
||||||
|
@ -6,3 +6,4 @@ export * from './text';
|
|||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './resource';
|
export * from './resource';
|
||||||
export * from './context';
|
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
|
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 {
|
export interface TextDimensionOptions {
|
||||||
// anything?
|
// anything?
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
ColorDimensionEditor,
|
ColorDimensionEditor,
|
||||||
ResourceDimensionEditor,
|
ResourceDimensionEditor,
|
||||||
ScaleDimensionEditor,
|
ScaleDimensionEditor,
|
||||||
|
ScalarDimensionEditor,
|
||||||
TextDimensionEditor,
|
TextDimensionEditor,
|
||||||
} from 'app/features/dimensions/editors';
|
} from 'app/features/dimensions/editors';
|
||||||
import {
|
import {
|
||||||
@ -27,14 +28,16 @@ import {
|
|||||||
ResourceFolderName,
|
ResourceFolderName,
|
||||||
TextDimensionConfig,
|
TextDimensionConfig,
|
||||||
defaultTextConfig,
|
defaultTextConfig,
|
||||||
|
ScalarDimensionConfig,
|
||||||
} from 'app/features/dimensions/types';
|
} 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 { styleUsesText } from '../../style/utils';
|
||||||
import { LayerContentInfo } from '../../utils/getFeatures';
|
import { LayerContentInfo } from '../../utils/getFeatures';
|
||||||
|
|
||||||
export interface StyleEditorOptions {
|
export interface StyleEditorOptions {
|
||||||
layerInfo?: Observable<LayerContentInfo>;
|
layerInfo?: Observable<LayerContentInfo>;
|
||||||
simpleFixedValues?: boolean;
|
simpleFixedValues?: boolean;
|
||||||
|
displayRotation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
||||||
@ -43,6 +46,8 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
onChange,
|
onChange,
|
||||||
item,
|
item,
|
||||||
}) => {
|
}) => {
|
||||||
|
const settings = item.settings;
|
||||||
|
|
||||||
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
||||||
onChange({ ...value, size: sizeValue });
|
onChange({ ...value, size: sizeValue });
|
||||||
};
|
};
|
||||||
@ -59,6 +64,10 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
onChange({ ...value, opacity: opacityValue });
|
onChange({ ...value, opacity: opacityValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRotationChange = (rotationValue: ScalarDimensionConfig | undefined) => {
|
||||||
|
onChange({ ...value, rotation: rotationValue });
|
||||||
|
};
|
||||||
|
|
||||||
const onTextChange = (textValue: TextDimensionConfig | undefined) => {
|
const onTextChange = (textValue: TextDimensionConfig | undefined) => {
|
||||||
onChange({ ...value, text: textValue });
|
onChange({ ...value, text: textValue });
|
||||||
};
|
};
|
||||||
@ -84,38 +93,54 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
};
|
};
|
||||||
|
|
||||||
let featuresHavePoints = false;
|
let featuresHavePoints = false;
|
||||||
if (item.settings?.layerInfo) {
|
if (settings?.layerInfo) {
|
||||||
const propertyOptions = useObservable(item.settings?.layerInfo);
|
const propertyOptions = useObservable(settings?.layerInfo);
|
||||||
featuresHavePoints = propertyOptions?.geometryType === 'point';
|
featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasTextLabel = styleUsesText(value);
|
const hasTextLabel = styleUsesText(value);
|
||||||
|
|
||||||
// Simple fixed value display
|
// Simple fixed value display
|
||||||
if (item.settings?.simpleFixedValues) {
|
if (settings?.simpleFixedValues) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{featuresHavePoints && (
|
{featuresHavePoints && (
|
||||||
<InlineFieldRow>
|
<>
|
||||||
<InlineField label={'Symbol'}>
|
<InlineFieldRow>
|
||||||
<ResourceDimensionEditor
|
<InlineField label={'Symbol'}>
|
||||||
value={value.symbol ?? defaultStyleConfig.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}
|
context={context}
|
||||||
onChange={onSymbolChange}
|
onChange={onRotationChange}
|
||||||
item={
|
item={
|
||||||
{
|
{
|
||||||
settings: {
|
settings: {
|
||||||
resourceType: 'icon',
|
min: defaultStyleConfig.rotation.min,
|
||||||
folderName: ResourceFolderName.Marker,
|
max: defaultStyleConfig.rotation.max,
|
||||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
|
||||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
|
||||||
showSourceRadio: false,
|
|
||||||
},
|
},
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</Field>
|
||||||
</InlineFieldRow>
|
</>
|
||||||
)}
|
)}
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Color" labelWidth={10}>
|
<InlineField label="Color" labelWidth={10}>
|
||||||
@ -210,6 +235,23 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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'}>
|
<Field label={'Text label'}>
|
||||||
<TextDimensionEditor
|
<TextDimensionEditor
|
||||||
value={value.text ?? defaultTextConfig}
|
value={value.text ?? defaultTextConfig}
|
||||||
|
@ -12,7 +12,7 @@ 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 { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
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 { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject } from 'rxjs';
|
||||||
@ -107,6 +107,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
if (style.fields.text) {
|
if (style.fields.text) {
|
||||||
dims.text = getTextDimension(frame, style.config.text!);
|
dims.text = getTextDimension(frame, style.config.text!);
|
||||||
}
|
}
|
||||||
|
if (style.fields.rotation) {
|
||||||
|
dims.rotation = getScalarDimension(frame, style.config.rotation ?? defaultStyleConfig.rotation);
|
||||||
|
}
|
||||||
style.dims = dims;
|
style.dims = dims;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +142,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
path: 'config.style',
|
path: 'config.style',
|
||||||
name: 'Styles',
|
name: 'Styles',
|
||||||
editor: StyleEditor,
|
editor: StyleEditor,
|
||||||
settings: {},
|
settings: {
|
||||||
|
displayRotation: true,
|
||||||
|
},
|
||||||
defaultValue: defaultOptions.style,
|
defaultValue: defaultOptions.style,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
|
@ -150,6 +150,12 @@ describe('geomap migrations', () => {
|
|||||||
"fixed": "dark-green",
|
"fixed": "dark-green",
|
||||||
},
|
},
|
||||||
"opacity": 0.4,
|
"opacity": 0.4,
|
||||||
|
"rotation": Object {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod",
|
||||||
|
},
|
||||||
"size": Object {
|
"size": Object {
|
||||||
"field": "Count",
|
"field": "Count",
|
||||||
"fixed": 5,
|
"fixed": 5,
|
||||||
|
@ -121,13 +121,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.square],
|
aliasIds: [MarkerShapePath.square],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
fill: getFillColor(cfg),
|
fill: getFillColor(cfg),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius,
|
radius,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -139,13 +140,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.triangle],
|
aliasIds: [MarkerShapePath.triangle],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
fill: getFillColor(cfg),
|
fill: getFillColor(cfg),
|
||||||
points: 3,
|
points: 3,
|
||||||
radius,
|
radius,
|
||||||
rotation: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
@ -158,6 +160,7 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.star],
|
aliasIds: [MarkerShapePath.star],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
@ -166,6 +169,7 @@ const makers: SymbolMaker[] = [
|
|||||||
radius,
|
radius,
|
||||||
radius2: radius * 0.4,
|
radius2: radius * 0.4,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -177,6 +181,7 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.cross],
|
aliasIds: [MarkerShapePath.cross],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
@ -184,6 +189,7 @@ const makers: SymbolMaker[] = [
|
|||||||
radius,
|
radius,
|
||||||
radius2: 0,
|
radius2: 0,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -195,13 +201,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.x],
|
aliasIds: [MarkerShapePath.x],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius,
|
radius,
|
||||||
radius2: 0,
|
radius2: 0,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -265,6 +272,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
|||||||
make: src
|
make: src
|
||||||
? (cfg: StyleConfigValues) => {
|
? (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return [
|
return [
|
||||||
new Style({
|
new Style({
|
||||||
image: new Icon({
|
image: new Icon({
|
||||||
@ -272,6 +280,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
|||||||
color: cfg.color,
|
color: cfg.color,
|
||||||
opacity: cfg.opacity ?? 1,
|
opacity: cfg.opacity ?? 1,
|
||||||
scale: (DEFAULT_SIZE + radius) / 100,
|
scale: (DEFAULT_SIZE + radius) / 100,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: !cfg?.text ? undefined : textLabel(cfg),
|
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)' }),
|
fill: new Fill({ color: 'rgba(0,0,0,0)' }),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius: cfg.size,
|
radius: cfg.size,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -4,6 +4,8 @@ import {
|
|||||||
ResourceDimensionConfig,
|
ResourceDimensionConfig,
|
||||||
ResourceDimensionMode,
|
ResourceDimensionMode,
|
||||||
ScaleDimensionConfig,
|
ScaleDimensionConfig,
|
||||||
|
ScalarDimensionConfig,
|
||||||
|
ScalarDimensionMode,
|
||||||
TextDimensionConfig,
|
TextDimensionConfig,
|
||||||
} from 'app/features/dimensions';
|
} from 'app/features/dimensions';
|
||||||
import { Style } from 'ol/style';
|
import { Style } from 'ol/style';
|
||||||
@ -30,6 +32,9 @@ export interface StyleConfig {
|
|||||||
// Can show markers and text together!
|
// Can show markers and text together!
|
||||||
text?: TextDimensionConfig;
|
text?: TextDimensionConfig;
|
||||||
textConfig?: TextStyleConfig;
|
textConfig?: TextStyleConfig;
|
||||||
|
|
||||||
|
// Allow for rotation of markers
|
||||||
|
rotation?: ScalarDimensionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIZE = 5;
|
export const DEFAULT_SIZE = 5;
|
||||||
@ -66,6 +71,12 @@ export const defaultStyleConfig = Object.freeze({
|
|||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
},
|
},
|
||||||
|
rotation: {
|
||||||
|
fixed: 0,
|
||||||
|
mode: ScalarDimensionMode.Mod,
|
||||||
|
min: -360,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,12 +110,14 @@ export interface StyleConfigFields {
|
|||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
rotation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StyleDimensions {
|
export interface StyleDimensions {
|
||||||
color?: DimensionSupplier<string>;
|
color?: DimensionSupplier<string>;
|
||||||
size?: DimensionSupplier<number>;
|
size?: DimensionSupplier<number>;
|
||||||
text?: DimensionSupplier<string>;
|
text?: DimensionSupplier<string>;
|
||||||
|
rotation?: DimensionSupplier<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StyleConfigState {
|
export interface StyleConfigState {
|
||||||
|
@ -35,7 +35,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
|||||||
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
||||||
lineWidth: cfg.lineWidth ?? 1,
|
lineWidth: cfg.lineWidth ?? 1,
|
||||||
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
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,
|
maker,
|
||||||
};
|
};
|
||||||
@ -46,6 +46,9 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
|||||||
if (cfg.size?.field?.length) {
|
if (cfg.size?.field?.length) {
|
||||||
fields.size = cfg.size.field;
|
fields.size = cfg.size.field;
|
||||||
}
|
}
|
||||||
|
if (cfg.rotation?.field?.length) {
|
||||||
|
fields.rotation = cfg.rotation.field;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
state.base.text = cfg.text?.fixed;
|
state.base.text = cfg.text?.fixed;
|
||||||
|
@ -31,6 +31,9 @@ export const getFeatures = (
|
|||||||
if (dims.size) {
|
if (dims.size) {
|
||||||
values.size = dims.size.get(i);
|
values.size = dims.size.get(i);
|
||||||
}
|
}
|
||||||
|
if (dims.rotation) {
|
||||||
|
values.rotation = dims.rotation.get(i);
|
||||||
|
}
|
||||||
if (dims.text) {
|
if (dims.text) {
|
||||||
values.text = dims.text.get(i);
|
values.text = dims.text.get(i);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user