Geomap: Add Symbol Alignment Options (#74293)
* Geomap: Add Symbol Anchor Options * Update displacement for svg case * For square and x, account for 45 degree offset * Simplify displacement function * Remove unused todo * Update test defaults to include anchor * Add missing anchor default to tests * Move displacement function into utils and add test * Simplify anchor position options UX * Change verbage to alignment and swap direction * Include missing alignment rename * Update tests --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
nmarrs
parent
0ea047cc09
commit
03baf210b3
@@ -31,7 +31,15 @@ import {
|
||||
} from 'app/features/dimensions/editors';
|
||||
import { ResourceFolderName, defaultTextConfig, MediaType } from 'app/features/dimensions/types';
|
||||
|
||||
import { defaultStyleConfig, GeometryTypeId, StyleConfig, TextAlignment, TextBaseline } from '../style/types';
|
||||
import {
|
||||
HorizontalAlign,
|
||||
VerticalAlign,
|
||||
defaultStyleConfig,
|
||||
GeometryTypeId,
|
||||
StyleConfig,
|
||||
TextAlignment,
|
||||
TextBaseline,
|
||||
} from '../style/types';
|
||||
import { styleUsesText } from '../style/utils';
|
||||
import { LayerContentInfo } from '../utils/getFeatures';
|
||||
|
||||
@@ -101,6 +109,14 @@ export const StyleEditor = (props: Props) => {
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline } });
|
||||
};
|
||||
|
||||
const onAlignHorizontalChange = (alignHorizontal: HorizontalAlign) => {
|
||||
onChange({ ...value, symbolAlign: { ...value.symbolAlign, horizontal: alignHorizontal } });
|
||||
};
|
||||
|
||||
const onAlignVerticalChange = (alignVertical: VerticalAlign) => {
|
||||
onChange({ ...value, symbolAlign: { ...value.symbolAlign, vertical: alignVertical } });
|
||||
};
|
||||
|
||||
const propertyOptions = useObservable(settings?.layerInfo ?? of());
|
||||
const featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
|
||||
const hasTextLabel = styleUsesText(value);
|
||||
@@ -200,24 +216,48 @@ export const StyleEditor = (props: Props) => {
|
||||
/>
|
||||
</Field>
|
||||
{!settings?.hideSymbol && (
|
||||
<Field label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: MediaType.Icon,
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<>
|
||||
<Field label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: MediaType.Icon,
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Symbol Vertical Align'}>
|
||||
<RadioButtonGroup
|
||||
value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical}
|
||||
onChange={onAlignVerticalChange}
|
||||
options={[
|
||||
{ value: VerticalAlign.Top, label: capitalize(VerticalAlign.Top) },
|
||||
{ value: VerticalAlign.Center, label: capitalize(VerticalAlign.Center) },
|
||||
{ value: VerticalAlign.Bottom, label: capitalize(VerticalAlign.Bottom) },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Symbol Horizontal Align'}>
|
||||
<RadioButtonGroup
|
||||
value={value?.symbolAlign?.horizontal ?? defaultStyleConfig.symbolAlign.horizontal}
|
||||
onChange={onAlignHorizontalChange}
|
||||
options={[
|
||||
{ value: HorizontalAlign.Left, label: capitalize(HorizontalAlign.Left) },
|
||||
{ value: HorizontalAlign.Center, label: capitalize(HorizontalAlign.Center) },
|
||||
{ value: HorizontalAlign.Right, label: capitalize(HorizontalAlign.Right) },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field label={'Color'}>
|
||||
<ColorDimensionEditor
|
||||
|
||||
@@ -79,6 +79,10 @@ describe('Worldmap Migrations', () => {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"symbolAlign": {
|
||||
"horizontal": "center",
|
||||
"vertical": "center",
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
@@ -222,6 +226,10 @@ describe('geomap migrations', () => {
|
||||
"fixed": "img/icons/marker/triangle.svg",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"symbolAlign": {
|
||||
"horizontal": "center",
|
||||
"vertical": "center",
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { config } from '@grafana/runtime';
|
||||
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
|
||||
|
||||
import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types';
|
||||
import { getDisplacement } from './utils';
|
||||
|
||||
interface SymbolMaker extends RegistryItem {
|
||||
aliasIds: string[];
|
||||
@@ -80,11 +81,13 @@ export const textMarker = (cfg: StyleConfigValues) => {
|
||||
|
||||
export const circleMarker = (cfg: StyleConfigValues) => {
|
||||
const stroke = new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 });
|
||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
stroke,
|
||||
fill: getFillColor(cfg),
|
||||
radius: cfg.size ?? DEFAULT_SIZE,
|
||||
radius,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
stroke, // in case lines are sent to the markers layer
|
||||
@@ -153,7 +156,9 @@ const makers: SymbolMaker[] = [
|
||||
fill: getFillColor(cfg),
|
||||
points: 4,
|
||||
radius,
|
||||
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||
angle: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@@ -174,6 +179,7 @@ const makers: SymbolMaker[] = [
|
||||
radius,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
angle: 0,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@@ -195,6 +201,7 @@ const makers: SymbolMaker[] = [
|
||||
radius2: radius * 0.4,
|
||||
angle: 0,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@@ -215,6 +222,7 @@ const makers: SymbolMaker[] = [
|
||||
radius2: 0,
|
||||
angle: 0,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@@ -233,7 +241,9 @@ const makers: SymbolMaker[] = [
|
||||
points: 4,
|
||||
radius,
|
||||
radius2: 0,
|
||||
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||
angle: Math.PI / 4,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
|
||||
}),
|
||||
text: textLabel(cfg),
|
||||
});
|
||||
@@ -315,6 +325,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
||||
opacity: cfg.opacity ?? 1,
|
||||
scale: (DEFAULT_SIZE + radius) / 100,
|
||||
rotation: (rotation * Math.PI) / 180,
|
||||
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius / 2),
|
||||
}),
|
||||
text: !cfg?.text ? undefined : textLabel(cfg),
|
||||
}),
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface StyleConfig {
|
||||
// Used for points and dynamic text
|
||||
size?: ScaleDimensionConfig;
|
||||
symbol?: ResourceDimensionConfig;
|
||||
symbolAlign?: SymbolAlign;
|
||||
|
||||
// Can show markers and text together!
|
||||
text?: TextDimensionConfig;
|
||||
@@ -50,6 +51,16 @@ export enum TextBaseline {
|
||||
Middle = 'middle',
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
export enum HorizontalAlign {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Right = 'right',
|
||||
}
|
||||
export enum VerticalAlign {
|
||||
Top = 'top',
|
||||
Center = 'center',
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export const defaultStyleConfig = Object.freeze({
|
||||
size: {
|
||||
@@ -65,6 +76,10 @@ export const defaultStyleConfig = Object.freeze({
|
||||
mode: ResourceDimensionMode.Fixed,
|
||||
fixed: 'img/icons/marker/circle.svg',
|
||||
},
|
||||
symbolAlign: {
|
||||
horizontal: HorizontalAlign.Center,
|
||||
vertical: VerticalAlign.Center,
|
||||
},
|
||||
textConfig: {
|
||||
fontSize: 12,
|
||||
textAlign: TextAlignment.Center,
|
||||
@@ -80,6 +95,11 @@ export const defaultStyleConfig = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
export interface SymbolAlign {
|
||||
horizontal?: HorizontalAlign;
|
||||
vertical?: VerticalAlign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static options for text display. See:
|
||||
* https://openlayers.org/en/latest/apidoc/module-ol_style_Text.html
|
||||
@@ -99,6 +119,7 @@ export interface StyleConfigValues {
|
||||
lineWidth?: number;
|
||||
size?: number;
|
||||
symbol?: string; // the point symbol
|
||||
symbolAlign?: SymbolAlign;
|
||||
rotation?: number;
|
||||
text?: string;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ResourceDimensionMode } from '@grafana/schema';
|
||||
|
||||
import { StyleConfig } from './types';
|
||||
import { getStyleConfigState } from './utils';
|
||||
import { HorizontalAlign, VerticalAlign, StyleConfig, SymbolAlign } from './types';
|
||||
import { getDisplacement, getStyleConfigState } from './utils';
|
||||
|
||||
describe('style utils', () => {
|
||||
it('should fill in default values', async () => {
|
||||
@@ -41,6 +41,10 @@ describe('style utils', () => {
|
||||
"opacity": 0.4,
|
||||
"rotation": 0,
|
||||
"size": 5,
|
||||
"symbolAlign": {
|
||||
"horizontal": "center",
|
||||
"vertical": "center",
|
||||
},
|
||||
},
|
||||
"config": null,
|
||||
"fields": {
|
||||
@@ -52,4 +56,22 @@ describe('style utils', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should return correct displacement array for top left', async () => {
|
||||
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Left, vertical: VerticalAlign.Top };
|
||||
const radius = 10;
|
||||
const displacement = getDisplacement(symbolAlign, radius);
|
||||
expect(displacement).toEqual([-10, 10]);
|
||||
});
|
||||
it('should return correct displacement array for bottom right', async () => {
|
||||
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Right, vertical: VerticalAlign.Bottom };
|
||||
const radius = 10;
|
||||
const displacement = getDisplacement(symbolAlign, radius);
|
||||
expect(displacement).toEqual([10, -10]);
|
||||
});
|
||||
it('should return correct displacement array for center center', async () => {
|
||||
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Center, vertical: VerticalAlign.Center };
|
||||
const radius = 10;
|
||||
const displacement = getDisplacement(symbolAlign, radius);
|
||||
expect(displacement).toEqual([0, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,15 @@ import { config } from '@grafana/runtime';
|
||||
import { TextDimensionMode } from '@grafana/schema';
|
||||
|
||||
import { getMarkerMaker } from './markers';
|
||||
import { defaultStyleConfig, StyleConfig, StyleConfigFields, StyleConfigState } from './types';
|
||||
import {
|
||||
HorizontalAlign,
|
||||
VerticalAlign,
|
||||
defaultStyleConfig,
|
||||
StyleConfig,
|
||||
StyleConfigFields,
|
||||
StyleConfigState,
|
||||
SymbolAlign,
|
||||
} from './types';
|
||||
|
||||
/** Indicate if the style wants to show text values */
|
||||
export function styleUsesText(config: StyleConfig): boolean {
|
||||
@@ -37,6 +45,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
||||
lineWidth: cfg.lineWidth ?? 1,
|
||||
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
||||
rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later
|
||||
symbolAlign: cfg.symbolAlign ?? defaultStyleConfig.symbolAlign,
|
||||
},
|
||||
maker,
|
||||
};
|
||||
@@ -66,3 +75,19 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Return a displacment array depending on alignment and icon radius */
|
||||
export function getDisplacement(symbolAlign: SymbolAlign, radius: number) {
|
||||
const displacement = [0, 0];
|
||||
if (symbolAlign?.horizontal === HorizontalAlign.Left) {
|
||||
displacement[0] = -radius;
|
||||
} else if (symbolAlign?.horizontal === HorizontalAlign.Right) {
|
||||
displacement[0] = radius;
|
||||
}
|
||||
if (symbolAlign?.vertical === VerticalAlign.Top) {
|
||||
displacement[1] = radius;
|
||||
} else if (symbolAlign?.vertical === VerticalAlign.Bottom) {
|
||||
displacement[1] = -radius;
|
||||
}
|
||||
return displacement;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user