mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] add Symbol orientation style property (#39129)
* add UI for symbol orientation styling * set icon-rotate layout property * fix jest test with new default style property value * remove some references to scaled and use styling instead
This commit is contained in:
parent
519eef17b1
commit
20bbb9c164
9 changed files with 199 additions and 29 deletions
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { dynamicOrientationShape } from '../style_option_shapes';
|
||||
import { FieldSelect, fieldShape } from '../field_select';
|
||||
|
||||
export function DynamicOrientationSelection({ ordinalFields, styleOptions, onChange }) {
|
||||
const onFieldChange = ({ field }) => {
|
||||
onChange({ ...styleOptions, field });
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldSelect
|
||||
fields={ordinalFields}
|
||||
selectedField={styleOptions.field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DynamicOrientationSelection.propTypes = {
|
||||
ordinalFields: PropTypes.arrayOf(fieldShape).isRequired,
|
||||
styleOptions: dynamicOrientationShape.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { StaticDynamicStyleRow } from '../../static_dynamic_style_row';
|
||||
import { DynamicOrientationSelection } from './dynamic_orientation_selection';
|
||||
import { StaticOrientationSelection } from './static_orientation_selection';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function OrientationEditor(props) {
|
||||
return (
|
||||
<StaticDynamicStyleRow
|
||||
ordinalFields={props.ordinalFields}
|
||||
property={props.styleProperty}
|
||||
label={i18n.translate('xpack.maps.styles.vector.orientationLabel', {
|
||||
defaultMessage: 'Symbol orientation'
|
||||
})}
|
||||
styleDescriptor={props.styleDescriptor}
|
||||
handlePropertyChange={props.handlePropertyChange}
|
||||
DynamicSelector={DynamicOrientationSelection}
|
||||
StaticSelector={StaticOrientationSelection}
|
||||
defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
|
||||
defaultStaticStyleOptions={props.defaultStaticStyleOptions}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { staticOrientationShape } from '../style_option_shapes';
|
||||
import { ValidatedRange } from '../../../../../components/validated_range';
|
||||
|
||||
export function StaticOrientationSelection({ onChange, styleOptions }) {
|
||||
|
||||
const onOrientationChange = (orientation) => {
|
||||
onChange({ orientation });
|
||||
};
|
||||
|
||||
return (
|
||||
<ValidatedRange
|
||||
min={0}
|
||||
max={360}
|
||||
value={styleOptions.orientation}
|
||||
onChange={onOrientationChange}
|
||||
showInput
|
||||
showRange
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StaticOrientationSelection.propTypes = {
|
||||
styleOptions: staticOrientationShape.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
|
@ -16,6 +16,14 @@ export const dynamicColorShape = PropTypes.shape({
|
|||
field: fieldShape,
|
||||
});
|
||||
|
||||
export const staticOrientationShape = PropTypes.shape({
|
||||
orientation: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
export const dynamicOrientationShape = PropTypes.shape({
|
||||
field: fieldShape,
|
||||
});
|
||||
|
||||
export const staticSizeShape = PropTypes.shape({
|
||||
size: PropTypes.number.isRequired,
|
||||
});
|
||||
|
@ -29,6 +37,8 @@ export const dynamicSizeShape = PropTypes.shape({
|
|||
export const styleOptionShapes = [
|
||||
staticColorShape,
|
||||
dynamicColorShape,
|
||||
staticOrientationShape,
|
||||
dynamicOrientationShape,
|
||||
staticSizeShape,
|
||||
dynamicSizeShape
|
||||
];
|
||||
|
|
|
@ -11,6 +11,7 @@ import chrome from 'ui/chrome';
|
|||
import { VectorStyleColorEditor } from './color/vector_style_color_editor';
|
||||
import { VectorStyleSizeEditor } from './size/vector_style_size_editor';
|
||||
import { VectorStyleSymbolEditor } from './vector_style_symbol_editor';
|
||||
import { OrientationEditor } from './orientation/orientation_editor';
|
||||
import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../../vector_style_defaults';
|
||||
import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types';
|
||||
import { SYMBOLIZE_AS_CIRCLE } from '../../vector_constants';
|
||||
|
@ -141,6 +142,7 @@ export class VectorStyleEditor extends Component {
|
|||
_renderPointProperties() {
|
||||
let lineColor;
|
||||
let lineWidth;
|
||||
let iconOrientation;
|
||||
if (this.props.styleProperties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE) {
|
||||
lineColor = (
|
||||
<Fragment>
|
||||
|
@ -154,16 +156,24 @@ export class VectorStyleEditor extends Component {
|
|||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
iconOrientation = (
|
||||
<Fragment>
|
||||
<OrientationEditor
|
||||
styleProperty="iconOrientation"
|
||||
handlePropertyChange={this.props.handlePropertyChange}
|
||||
styleDescriptor={this.props.styleProperties.iconOrientation}
|
||||
ordinalFields={this.state.ordinalFields}
|
||||
defaultStaticStyleOptions={this.state.defaultStaticProperties.iconOrientation.options}
|
||||
defaultDynamicStyleOptions={this.state.defaultDynamicProperties.iconOrientation.options}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<VectorStyleSymbolEditor
|
||||
styleOptions={this.props.styleProperties.symbol.options}
|
||||
handlePropertyChange={this.props.handlePropertyChange}
|
||||
symbolOptions={SYMBOL_OPTIONS}
|
||||
isDarkMode={chrome.getUiSettingsClient().get('theme:darkMode', false)}
|
||||
/>
|
||||
|
||||
{this._renderFillColor()}
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -172,7 +182,17 @@ export class VectorStyleEditor extends Component {
|
|||
|
||||
{lineWidth}
|
||||
|
||||
<VectorStyleSymbolEditor
|
||||
styleOptions={this.props.styleProperties.symbol.options}
|
||||
handlePropertyChange={this.props.handlePropertyChange}
|
||||
symbolOptions={SYMBOL_OPTIONS}
|
||||
isDarkMode={chrome.getUiSettingsClient().get('theme:darkMode', false)}
|
||||
/>
|
||||
|
||||
{iconOrientation}
|
||||
|
||||
{this._renderSymbolSize()}
|
||||
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -314,21 +314,26 @@ export class VectorStyle extends AbstractStyle {
|
|||
return (<VectorStyleLegend styleProperties={styleProperties}/>);
|
||||
}
|
||||
|
||||
_getScaledFields() {
|
||||
_getStyleFields() {
|
||||
return this.getDynamicPropertiesArray()
|
||||
.map(({ styleName, options }) => {
|
||||
const name = options.field.name;
|
||||
|
||||
// "feature-state" data expressions are not supported with layout properties.
|
||||
// To work around this limitation, some scaled values must fall back to geojson property values.
|
||||
// To work around this limitation, some styling values must fall back to geojson property values.
|
||||
let supportsFeatureState = true;
|
||||
let isScaled = true;
|
||||
if (styleName === 'iconSize'
|
||||
&& this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) {
|
||||
supportsFeatureState = false;
|
||||
} else if (styleName === 'iconOrientation') {
|
||||
supportsFeatureState = false;
|
||||
isScaled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
supportsFeatureState,
|
||||
isScaled,
|
||||
name,
|
||||
range: this._getFieldRange(name),
|
||||
computedName: VectorStyle.getComputedFieldName(name),
|
||||
|
@ -355,8 +360,8 @@ export class VectorStyle extends AbstractStyle {
|
|||
return;
|
||||
}
|
||||
|
||||
const scaledFields = this._getScaledFields();
|
||||
if (scaledFields.length === 0) {
|
||||
const styleFields = this._getStyleFields();
|
||||
if (styleFields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -370,21 +375,30 @@ export class VectorStyle extends AbstractStyle {
|
|||
for (let i = 0; i < featureCollection.features.length; i++) {
|
||||
const feature = featureCollection.features[i];
|
||||
|
||||
for (let j = 0; j < scaledFields.length; j++) {
|
||||
const { supportsFeatureState, name, range, computedName } = scaledFields[j];
|
||||
const unscaledValue = parseFloat(feature.properties[name]);
|
||||
let scaledValue;
|
||||
if (isNaN(unscaledValue) || !range) {//cannot scale
|
||||
scaledValue = -1;//put outside range
|
||||
} else if (range.delta === 0) {//values are identical
|
||||
scaledValue = 1;//snap to end of color range
|
||||
for (let j = 0; j < styleFields.length; j++) {
|
||||
const { supportsFeatureState, isScaled, name, range, computedName } = styleFields[j];
|
||||
const value = parseFloat(feature.properties[name]);
|
||||
let styleValue;
|
||||
if (isScaled) {
|
||||
if (isNaN(value) || !range) {//cannot scale
|
||||
styleValue = -1;//put outside range
|
||||
} else if (range.delta === 0) {//values are identical
|
||||
styleValue = 1;//snap to end of color range
|
||||
} else {
|
||||
styleValue = (feature.properties[name] - range.min) / range.delta;
|
||||
}
|
||||
} else {
|
||||
scaledValue = (feature.properties[name] - range.min) / range.delta;
|
||||
if (isNaN(value)) {
|
||||
styleValue = 0;
|
||||
} else {
|
||||
styleValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsFeatureState) {
|
||||
tmpFeatureState[computedName] = scaledValue;
|
||||
tmpFeatureState[computedName] = styleValue;
|
||||
} else {
|
||||
feature.properties[computedName] = scaledValue;
|
||||
feature.properties[computedName] = styleValue;
|
||||
}
|
||||
}
|
||||
tmpFeatureIdentifier.source = sourceId;
|
||||
|
@ -392,10 +406,10 @@ export class VectorStyle extends AbstractStyle {
|
|||
mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState);
|
||||
}
|
||||
|
||||
const hasScaledGeoJsonProperties = scaledFields.some(({ supportsFeatureState }) => {
|
||||
const hasGeoJsonProperties = styleFields.some(({ supportsFeatureState }) => {
|
||||
return !supportsFeatureState;
|
||||
});
|
||||
return hasScaledGeoJsonProperties;
|
||||
return hasGeoJsonProperties;
|
||||
}
|
||||
|
||||
_getMBDataDrivenColor({ fieldName, color }) {
|
||||
|
@ -559,6 +573,17 @@ export class VectorStyle extends AbstractStyle {
|
|||
1, iconSize.options.maxSize / halfIconPixels
|
||||
]);
|
||||
}
|
||||
|
||||
const iconOrientation = this._descriptor.properties.iconOrientation;
|
||||
if (iconOrientation.type === VectorStyle.STYLE_TYPE.STATIC) {
|
||||
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', iconOrientation.options.orientation);
|
||||
} else if (_.has(iconOrientation, 'options.field.name')) {
|
||||
const targetName = VectorStyle.getComputedFieldName(iconOrientation.options.field.name);
|
||||
// Using property state instead of feature-state because layout properties do not support feature-state
|
||||
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', [
|
||||
'coalesce', ['get', targetName], 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
arePointsSymbolizedAsCircles() {
|
||||
|
|
|
@ -49,6 +49,12 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
|
|||
options: {},
|
||||
type: 'STATIC',
|
||||
},
|
||||
iconOrientation: {
|
||||
options: {
|
||||
orientation: 0,
|
||||
},
|
||||
type: 'STATIC',
|
||||
},
|
||||
iconSize: {
|
||||
options: {
|
||||
color: 'a color',
|
||||
|
|
|
@ -56,7 +56,13 @@ export function getDefaultStaticProperties(mapColors = []) {
|
|||
options: {
|
||||
size: DEFAULT_ICON_SIZE
|
||||
}
|
||||
}
|
||||
},
|
||||
iconOrientation: {
|
||||
type: VectorStyle.STYLE_TYPE.STATIC,
|
||||
options: {
|
||||
orientation: 0
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -66,27 +72,37 @@ export function getDefaultDynamicProperties() {
|
|||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
field: undefined,
|
||||
}
|
||||
},
|
||||
lineColor: {
|
||||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
color: COLOR_GRADIENTS[0].value,
|
||||
field: undefined,
|
||||
}
|
||||
},
|
||||
lineWidth: {
|
||||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
minSize: DEFAULT_MIN_SIZE,
|
||||
maxSize: DEFAULT_MAX_SIZE
|
||||
maxSize: DEFAULT_MAX_SIZE,
|
||||
field: undefined,
|
||||
}
|
||||
},
|
||||
iconSize: {
|
||||
type: VectorStyle.STYLE_TYPE.DYNAMIC,
|
||||
options: {
|
||||
minSize: DEFAULT_MIN_SIZE,
|
||||
maxSize: DEFAULT_MAX_SIZE
|
||||
maxSize: DEFAULT_MAX_SIZE,
|
||||
field: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
iconOrientation: {
|
||||
type: VectorStyle.STYLE_TYPE.STATIC,
|
||||
options: {
|
||||
field: undefined,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -441,12 +441,12 @@ export class VectorLayer extends AbstractLayer {
|
|||
mbGeoJSONSource.setData(featureCollection);
|
||||
}
|
||||
|
||||
const hasScaledGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId());
|
||||
const hasGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId());
|
||||
|
||||
// "feature-state" data expressions are not supported with layout properties.
|
||||
// To work around this limitation,
|
||||
// scaled layout properties (like icon-size) must fall back to geojson property values :(
|
||||
if (hasScaledGeoJsonProperties) {
|
||||
if (hasGeoJsonProperties) {
|
||||
mbGeoJSONSource.setData(featureCollection);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue