[Maps] only show styles that apply to layer feature types in legend (#52335)
* [Maps] only show styles that apply to layer feature types in legend * update hasLegendDetails check to include style property filters * clean up
This commit is contained in:
parent
942f5420ed
commit
cb60a77bb9
|
@ -21,12 +21,14 @@ export class TOCEntry extends React.Component {
|
|||
|
||||
state = {
|
||||
displayName: null,
|
||||
hasLegendDetails: false,
|
||||
shouldShowModal: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._updateDisplayName();
|
||||
this._loadHasLegendDetails();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -35,6 +37,7 @@ export class TOCEntry extends React.Component {
|
|||
|
||||
componentDidUpdate() {
|
||||
this._updateDisplayName();
|
||||
this._loadHasLegendDetails();
|
||||
}
|
||||
|
||||
_toggleLayerDetailsVisibility = () => {
|
||||
|
@ -45,6 +48,13 @@ export class TOCEntry extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async _loadHasLegendDetails() {
|
||||
const hasLegendDetails = await this.props.layer.hasLegendDetails();
|
||||
if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) {
|
||||
this.setState({ hasLegendDetails });
|
||||
}
|
||||
}
|
||||
|
||||
async _updateDisplayName() {
|
||||
const label = await this.props.layer.getDisplayName();
|
||||
if (this._isMounted) {
|
||||
|
@ -143,7 +153,7 @@ export class TOCEntry extends React.Component {
|
|||
}
|
||||
|
||||
_renderDetailsToggle() {
|
||||
if (!this.props.layer.hasLegendDetails()) {
|
||||
if (!this.state.hasLegendDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -223,7 +233,7 @@ export class TOCEntry extends React.Component {
|
|||
}
|
||||
|
||||
_renderLegendDetails = () => {
|
||||
if (!this.props.isLegendDetailsOpen || !this.props.layer.hasLegendDetails()) {
|
||||
if (!this.props.isLegendDetailsOpen || !this.state.hasLegendDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ const LAYER_ID = '1';
|
|||
|
||||
const mockLayer = {
|
||||
getId: () => { return LAYER_ID; },
|
||||
hasLegendDetails: async () => { return true; },
|
||||
renderLegendDetails: () => { return (<div>TOC details mock</div>); },
|
||||
getDisplayName: () => { return 'layer 1'; },
|
||||
isVisible: () => { return true; },
|
||||
|
|
|
@ -98,7 +98,7 @@ export class HeatmapLayer extends VectorLayer {
|
|||
return 'heatmap';
|
||||
}
|
||||
|
||||
hasLegendDetails() {
|
||||
async hasLegendDetails() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -186,7 +186,7 @@ export class AbstractLayer {
|
|||
};
|
||||
}
|
||||
|
||||
hasLegendDetails() {
|
||||
async hasLegendDetails() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,77 +16,16 @@ const EMPTY_VALUE = '';
|
|||
|
||||
export class StylePropertyLegendRow extends Component {
|
||||
|
||||
state = {
|
||||
label: '',
|
||||
hasLoadedFieldFormatter: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._prevLabel = undefined;
|
||||
this._fieldValueFormatter = undefined;
|
||||
this._loadLabel();
|
||||
this._loadFieldFormatter();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// label could change so it needs to be loaded on update
|
||||
this._loadLabel();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
async _loadFieldFormatter() {
|
||||
if (this.props.style.isDynamic() && this.props.style.isComplete() && this.props.style.getField().getSource()) {
|
||||
const field = this.props.style.getField();
|
||||
const source = field.getSource();
|
||||
this._fieldValueFormatter = await source.getFieldFormatter(field.getName());
|
||||
} else {
|
||||
this._fieldValueFormatter = null;
|
||||
}
|
||||
if (this._isMounted) {
|
||||
this.setState({ hasLoadedFieldFormatter: true });
|
||||
}
|
||||
}
|
||||
|
||||
_loadLabel = async () => {
|
||||
if (this._excludeFromHeader()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// have to load label and then check for changes since field name stays constant while label may change
|
||||
const label = await this.props.style.getField().getLabel();
|
||||
if (this._prevLabel === label) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._prevLabel = label;
|
||||
if (this._isMounted) {
|
||||
this.setState({ label });
|
||||
}
|
||||
}
|
||||
|
||||
_excludeFromHeader() {
|
||||
return !this.props.style.isDynamic() || !this.props.style.isComplete() || !this.props.style.getField().getName();
|
||||
}
|
||||
|
||||
_formatValue = value => {
|
||||
if (!this.state.hasLoadedFieldFormatter || !this._fieldValueFormatter || value === EMPTY_VALUE) {
|
||||
if (!this.props.fieldFormatter || value === EMPTY_VALUE) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return this._fieldValueFormatter(value);
|
||||
return this.props.fieldFormatter(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { range, style } = this.props;
|
||||
if (this._excludeFromHeader()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = style.renderHeader();
|
||||
|
||||
const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE));
|
||||
const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min;
|
||||
|
@ -96,17 +35,19 @@ export class StylePropertyLegendRow extends Component {
|
|||
|
||||
return (
|
||||
<StyleLegendRow
|
||||
header={header}
|
||||
header={style.renderHeader()}
|
||||
minLabel={minLabel}
|
||||
maxLabel={maxLabel}
|
||||
propertyLabel={getVectorStyleLabel(style.getStyleName())}
|
||||
fieldLabel={this.state.label}
|
||||
fieldLabel={this.props.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StylePropertyLegendRow.propTypes = {
|
||||
label: PropTypes.string,
|
||||
fieldFormatter: PropTypes.func,
|
||||
range: rangeShape,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
|
|
@ -4,29 +4,59 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { rangeShape } from '../style_option_shapes';
|
||||
import { StylePropertyLegendRow } from './style_property_legend_row';
|
||||
|
||||
export function VectorStyleLegend({ styleProperties }) {
|
||||
return styleProperties.map(styleProperty => {
|
||||
return (
|
||||
<StylePropertyLegendRow
|
||||
style={styleProperty.style}
|
||||
key={styleProperty.style.getStyleName()}
|
||||
range={styleProperty.range}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export class VectorStyleLegend extends Component {
|
||||
|
||||
state = {
|
||||
rows: [],
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._prevRowDescriptors = undefined;
|
||||
this._loadRows();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._loadRows();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
_loadRows = _.debounce(async () => {
|
||||
const rows = await this.props.loadRows();
|
||||
const rowDescriptors = rows.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
range: row.range,
|
||||
styleOptions: row.style.getOptions(),
|
||||
};
|
||||
});
|
||||
if (this._isMounted && !_.isEqual(rowDescriptors, this._prevRowDescriptors)) {
|
||||
this._prevRowDescriptors = rowDescriptors;
|
||||
this.setState({ rows });
|
||||
}
|
||||
}, 100);
|
||||
|
||||
render() {
|
||||
return this.state.rows.map(rowProps => {
|
||||
return (
|
||||
<StylePropertyLegendRow
|
||||
key={rowProps.style.getStyleName()}
|
||||
{...rowProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stylePropertyShape = PropTypes.shape({
|
||||
range: rangeShape,
|
||||
style: PropTypes.object
|
||||
});
|
||||
|
||||
VectorStyleLegend.propTypes = {
|
||||
styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired
|
||||
loadRows: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -12,6 +12,24 @@ export function getComputedFieldNamePrefix(fieldName) {
|
|||
return `__kbn__dynamic__${fieldName}`;
|
||||
}
|
||||
|
||||
export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatureType) {
|
||||
if (supportedFeatures.length === 1) {
|
||||
return supportedFeatures[0] === featureType;
|
||||
}
|
||||
|
||||
if (!hasFeatureType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const featureTypes = Object.keys(hasFeatureType);
|
||||
return featureTypes.reduce((isOnlyTargetFeatureType, featureTypeKey) => {
|
||||
const hasFeature = hasFeatureType[featureTypeKey];
|
||||
return featureTypeKey === featureType
|
||||
? isOnlyTargetFeatureType && hasFeature
|
||||
: isOnlyTargetFeatureType && !hasFeature;
|
||||
}, true);
|
||||
}
|
||||
|
||||
export function scaleValue(value, range) {
|
||||
if (isNaN(value) || !range) {
|
||||
return -1; //Nothing to scale, put outside scaled range
|
||||
|
|
|
@ -4,7 +4,57 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { scaleValue } from './style_util';
|
||||
import { isOnlySingleFeatureType, scaleValue } from './style_util';
|
||||
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
|
||||
|
||||
describe('isOnlySingleFeatureType', () => {
|
||||
describe('source supports single feature type', () => {
|
||||
const supportedFeatures = [VECTOR_SHAPE_TYPES.POINT];
|
||||
|
||||
test('Is only single feature type when only supported feature type is target feature type', () => {
|
||||
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures)).toBe(true);
|
||||
});
|
||||
|
||||
test('Is not single feature type when only supported feature type is not target feature type', () => {
|
||||
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source supports multiple feature types', () => {
|
||||
const supportedFeatures = [
|
||||
VECTOR_SHAPE_TYPES.POINT,
|
||||
VECTOR_SHAPE_TYPES.LINE,
|
||||
VECTOR_SHAPE_TYPES.POLYGON
|
||||
];
|
||||
|
||||
test('Is only single feature type when data only has target feature type', () => {
|
||||
const hasFeatureType = {
|
||||
[VECTOR_SHAPE_TYPES.POINT]: true,
|
||||
[VECTOR_SHAPE_TYPES.LINE]: false,
|
||||
[VECTOR_SHAPE_TYPES.POLYGON]: false,
|
||||
};
|
||||
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType)).toBe(true);
|
||||
});
|
||||
|
||||
test('Is not single feature type when data has multiple feature types', () => {
|
||||
const hasFeatureType = {
|
||||
[VECTOR_SHAPE_TYPES.POINT]: true,
|
||||
[VECTOR_SHAPE_TYPES.LINE]: true,
|
||||
[VECTOR_SHAPE_TYPES.POLYGON]: true,
|
||||
};
|
||||
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures, hasFeatureType)).toBe(false);
|
||||
});
|
||||
|
||||
test('Is not single feature type when data does not have target feature types', () => {
|
||||
const hasFeatureType = {
|
||||
[VECTOR_SHAPE_TYPES.POINT]: false,
|
||||
[VECTOR_SHAPE_TYPES.LINE]: true,
|
||||
[VECTOR_SHAPE_TYPES.POLYGON]: false,
|
||||
};
|
||||
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('scaleValue', () => {
|
||||
test('Should scale value between 0 and 1', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { VectorStyleEditor } from './components/vector_style_editor';
|
||||
import { getDefaultProperties, VECTOR_STYLES } from './vector_style_defaults';
|
||||
import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES, VECTOR_STYLES } from './vector_style_defaults';
|
||||
import { AbstractStyle } from '../abstract_style';
|
||||
import {
|
||||
GEO_JSON_TYPE,
|
||||
|
@ -21,7 +21,7 @@ import { VectorStyleLegend } from './components/legend/vector_style_legend';
|
|||
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
|
||||
import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants';
|
||||
import { getMakiSymbolAnchor } from './symbol_utils';
|
||||
import { getComputedFieldName, scaleValue } from './style_util';
|
||||
import { getComputedFieldName, isOnlySingleFeatureType, scaleValue } from './style_util';
|
||||
import { StaticStyleProperty } from './properties/static_style_property';
|
||||
import { DynamicStyleProperty } from './properties/dynamic_style_property';
|
||||
import { DynamicSizeProperty } from './properties/dynamic_size_property';
|
||||
|
@ -271,32 +271,23 @@ export class VectorStyle extends AbstractStyle {
|
|||
return styleProperties.filter(styleProperty => (styleProperty.isDynamic() && styleProperty.isComplete()));
|
||||
}
|
||||
|
||||
_checkIfOnlyFeatureType = async (featureType) => {
|
||||
const supportedFeatures = await this._source.getSupportedShapeTypes();
|
||||
|
||||
if (supportedFeatures.length === 1) {
|
||||
return supportedFeatures[0] === featureType;
|
||||
}
|
||||
|
||||
if (!this._descriptor.__styleMeta || !this._descriptor.__styleMeta.hasFeatureType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const featureTypes = Object.keys(this._descriptor.__styleMeta.hasFeatureType);
|
||||
return featureTypes.reduce((isOnlySingleFeatureType, featureTypeKey) => {
|
||||
const hasFeature = this._descriptor.__styleMeta.hasFeatureType[featureTypeKey];
|
||||
return featureTypeKey === featureType
|
||||
? isOnlySingleFeatureType && hasFeature
|
||||
: isOnlySingleFeatureType && !hasFeature;
|
||||
}, true);
|
||||
_isOnlySingleFeatureType = async (featureType) => {
|
||||
return isOnlySingleFeatureType(
|
||||
featureType,
|
||||
await this._source.getSupportedShapeTypes(),
|
||||
this._getStyleMeta().hasFeatureType);
|
||||
}
|
||||
|
||||
_getIsPointsOnly = async () => {
|
||||
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.POINT);
|
||||
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT);
|
||||
}
|
||||
|
||||
_getIsLinesOnly = async () => {
|
||||
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.LINE);
|
||||
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE);
|
||||
}
|
||||
|
||||
_getIsPolygonsOnly = async () => {
|
||||
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POLYGON);
|
||||
}
|
||||
|
||||
_getFieldRange = (fieldName) => {
|
||||
|
@ -352,6 +343,10 @@ export class VectorStyle extends AbstractStyle {
|
|||
};
|
||||
}
|
||||
|
||||
_getStyleMeta = () => {
|
||||
return _.get(this._descriptor, '__styleMeta', {});
|
||||
}
|
||||
|
||||
getIcon = () => {
|
||||
const styles = this.getRawProperties();
|
||||
const symbolId = this.arePointsSymbolizedAsCircles()
|
||||
|
@ -368,21 +363,43 @@ export class VectorStyle extends AbstractStyle {
|
|||
);
|
||||
}
|
||||
|
||||
renderLegendDetails() {
|
||||
const styles = this._getAllStyleProperties();
|
||||
const styleProperties = styles.map((style) => {
|
||||
return {
|
||||
// eslint-disable-next-line max-len
|
||||
range: (style.isDynamic() && style.isComplete() && style.getField().getName()) ? this._getFieldRange(style.getField().getName()) : null,
|
||||
style: style
|
||||
};
|
||||
});
|
||||
async _getLegendDetailStyleProperties() {
|
||||
const isLinesOnly = await this._getIsLinesOnly();
|
||||
const isPolygonsOnly = await this._getIsPolygonsOnly();
|
||||
|
||||
return (
|
||||
<VectorStyleLegend
|
||||
styleProperties={styleProperties}
|
||||
/>
|
||||
);
|
||||
return this.getDynamicPropertiesArray().filter(styleProperty => {
|
||||
if (isLinesOnly) {
|
||||
return LINE_STYLES.includes(styleProperty.getStyleName());
|
||||
}
|
||||
|
||||
if (isPolygonsOnly) {
|
||||
return POLYGON_STYLES.includes(styleProperty.getStyleName());
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async hasLegendDetails() {
|
||||
const styles = await this._getLegendDetailStyleProperties();
|
||||
return styles.length > 0;
|
||||
}
|
||||
|
||||
renderLegendDetails() {
|
||||
const loadRows = async () => {
|
||||
const styles = await this._getLegendDetailStyleProperties();
|
||||
const promises = styles.map(async (style) => {
|
||||
return {
|
||||
label: await style.getField().getLabel(),
|
||||
fieldFormatter: await this._source.getFieldFormatter(style.getField().getName()),
|
||||
range: this._getFieldRange(style.getField().getName()),
|
||||
style,
|
||||
};
|
||||
});
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
return <VectorStyleLegend loadRows={loadRows} />;
|
||||
}
|
||||
|
||||
_getStyleFields() {
|
||||
|
|
|
@ -27,6 +27,9 @@ export const VECTOR_STYLES = {
|
|||
ICON_ORIENTATION: 'iconOrientation'
|
||||
};
|
||||
|
||||
export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH];
|
||||
export const POLYGON_STYLES = [VECTOR_STYLES.FILL_COLOR, VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH];
|
||||
|
||||
export function getDefaultProperties(mapColors = []) {
|
||||
return {
|
||||
...getDefaultStaticProperties(mapColors),
|
||||
|
|
|
@ -145,8 +145,8 @@ export class VectorLayer extends AbstractLayer {
|
|||
return 'vector';
|
||||
}
|
||||
|
||||
hasLegendDetails() {
|
||||
return this._style.getDynamicPropertiesArray().length > 0;
|
||||
async hasLegendDetails() {
|
||||
return this._style.hasLegendDetails();
|
||||
}
|
||||
|
||||
renderLegendDetails() {
|
||||
|
|
Loading…
Reference in a new issue