[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:
Nathan Reese 2019-06-18 18:09:33 -06:00 committed by GitHub
parent 519eef17b1
commit 20bbb9c164
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 29 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,6 +49,12 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
options: {},
type: 'STATIC',
},
iconOrientation: {
options: {
orientation: 0,
},
type: 'STATIC',
},
iconSize: {
options: {
color: 'a color',

View file

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

View file

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