mirror of
https://github.com/maputnik/editor.git
synced 2026-01-04 20:40:01 +00:00
Color relief support and hillshading improvements. (#1371)
## Launch Checklist This adds support for `relief-color` property so that it will create an elevation expression when the button is pressed. It also adds support for `colorArray` and `numberArray` types so that the user would be able to add the relevant information. Before: <img width="403" height="324" alt="image" src="https://github.com/user-attachments/assets/250abd81-6176-4711-a1ee-d33d443932d7" /> <img width="403" height="324" alt="image" src="https://github.com/user-attachments/assets/6a1bb268-66db-42a1-97fc-33e5a40863b6" /> After: <img width="403" height="324" alt="image" src="https://github.com/user-attachments/assets/8ebaa1ea-4ef9-4aed-abcd-3c8b0057ea76" /> <img width="403" height="324" alt="image" src="https://github.com/user-attachments/assets/e0728c92-85f9-4b86-8635-8877cf257b2f" /> - [x] Briefly describe the changes in this PR. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
type BlockProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
|
||||
@@ -90,6 +90,18 @@ function getDataType(value: any, fieldSpec={} as any) {
|
||||
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "colorArray") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "padding") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
|
||||
return "value";
|
||||
}
|
||||
else if (isZoomField(value)) {
|
||||
return "zoom_function";
|
||||
}
|
||||
@@ -293,6 +305,20 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
props.onChange(props.fieldName, dataFunc);
|
||||
};
|
||||
|
||||
const makeElevationFunction = () => {
|
||||
const expression = [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["elevation"],
|
||||
0,
|
||||
"black",
|
||||
2000,
|
||||
"white"
|
||||
];
|
||||
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
|
||||
const onMarkEditing = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
@@ -364,6 +390,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
onZoomClick={makeZoomFunction}
|
||||
onDataClick={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
onElevationClick={makeElevationFunction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
import Block from './Block'
|
||||
import InputSpec, { InputSpecProps } from './InputSpec'
|
||||
import InputSpec, { FieldSpecType, InputSpecProps } from './InputSpec'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
const typeMap = {
|
||||
color: () => Block,
|
||||
enum: ({fieldSpec}: any) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block),
|
||||
boolean: () => Block,
|
||||
array: () => Fieldset,
|
||||
resolvedImage: () => Block,
|
||||
number: () => Block,
|
||||
string: () => Block,
|
||||
formatted: () => Block,
|
||||
padding: () => Block,
|
||||
};
|
||||
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
|
||||
switch(fieldSpec.type) {
|
||||
case 'color':
|
||||
return Block;
|
||||
case 'enum':
|
||||
return (Object.keys(fieldSpec.values!).length <= 3 ? Fieldset : Block)
|
||||
case 'boolean':
|
||||
return Block;
|
||||
case 'array':
|
||||
return Fieldset;
|
||||
case 'resolvedImage':
|
||||
return Block;
|
||||
case 'number':
|
||||
return Block;
|
||||
case 'string':
|
||||
return Block;
|
||||
case 'formatted':
|
||||
return Block;
|
||||
case 'padding':
|
||||
return Block;
|
||||
case 'numberArray':
|
||||
return Fieldset;
|
||||
case 'colorArray':
|
||||
return Fieldset;
|
||||
case 'variableAnchorOffsetCollection':
|
||||
return Fieldset;
|
||||
default:
|
||||
console.warn("No such type for: " + fieldSpec.type);
|
||||
return Block;
|
||||
}
|
||||
}
|
||||
|
||||
export type FieldSpecProps = InputSpecProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
|
||||
const fieldType = props.fieldSpec?.type;
|
||||
|
||||
const typeBlockFn = typeMap[fieldType!];
|
||||
|
||||
let TypeBlock;
|
||||
if (typeBlockFn) {
|
||||
TypeBlock = typeBlockFn(props);
|
||||
}
|
||||
else {
|
||||
console.warn("No such type for '%s'", fieldType);
|
||||
TypeBlock = Block;
|
||||
}
|
||||
const TypeBlock = getElementFromType(props.fieldSpec!);
|
||||
|
||||
return (
|
||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {CSSProperties} from 'react'
|
||||
import { BsDiamond, BsDiamondFill, BsFonts } from 'react-icons/bs'
|
||||
import { MdOutlineCircle, MdPriorityHigh } from 'react-icons/md'
|
||||
import { BsDiamond, BsDiamondFill, BsFonts, BsSun } from 'react-icons/bs'
|
||||
import { MdBubbleChart, MdOutlineCircle, MdPriorityHigh } from 'react-icons/md'
|
||||
import { IoAnalyticsOutline } from 'react-icons/io5'
|
||||
|
||||
type IconLayerProps = {
|
||||
@@ -16,7 +16,8 @@ const IconLayer: React.FC<IconLayerProps> = (props) => {
|
||||
switch(props.type) {
|
||||
case 'fill-extrusion': return <BsDiamondFill {...iconProps} />
|
||||
case 'raster': return <BsDiamond {...iconProps} />
|
||||
case 'hillshade': return <BsDiamond {...iconProps} />
|
||||
case 'hillshade': return <BsSun {...iconProps} />
|
||||
case 'color-relief': return <MdBubbleChart {...iconProps} />
|
||||
case 'heatmap': return <BsDiamond {...iconProps} />
|
||||
case 'fill': return <BsDiamond {...iconProps} />
|
||||
case 'background': return <BsDiamondFill {...iconProps} />
|
||||
|
||||
@@ -9,11 +9,12 @@ import InputButton from './InputButton'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import InputEnum from './InputEnum'
|
||||
import InputUrl from './InputUrl'
|
||||
import InputColor from './InputColor';
|
||||
|
||||
|
||||
export type InputDynamicArrayProps = {
|
||||
value?: (string | number | undefined)[]
|
||||
type?: 'url' | 'number' | 'enum' | 'string'
|
||||
type?: 'url' | 'number' | 'enum' | 'string' | 'color'
|
||||
default?: (string | number | undefined)[]
|
||||
onChange?(values: (string | number | undefined)[] | undefined): unknown
|
||||
style?: object
|
||||
@@ -49,6 +50,8 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
||||
const {fieldSpec} = this.props;
|
||||
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
||||
values.push(defaultValue);
|
||||
} else if (this.props.type === 'color') {
|
||||
values.push("#000000");
|
||||
} else {
|
||||
values.push("")
|
||||
}
|
||||
@@ -95,6 +98,13 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else if (this.props.type === 'color') {
|
||||
input = <InputColor
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else {
|
||||
input = <InputString
|
||||
value={v as string}
|
||||
|
||||
@@ -10,10 +10,11 @@ import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/addon/lint/lint.css'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import '../libs/codemirror-mgl';
|
||||
import type { LayerSpecification } from 'maplibre-gl';
|
||||
|
||||
|
||||
export type InputJsonProps = {
|
||||
layer: any
|
||||
layer: LayerSpecification
|
||||
maxHeight?: number
|
||||
onChange?(...args: unknown[]): unknown
|
||||
lineNumbers?: boolean
|
||||
|
||||
@@ -13,12 +13,14 @@ import capitalize from 'lodash.capitalize'
|
||||
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
export type FieldSpecType = 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array' | 'numberArray' | 'padding' | 'colorArray' | 'variableAnchorOffsetCollection';
|
||||
|
||||
export type InputSpecProps = {
|
||||
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
|
||||
fieldName?: string
|
||||
fieldSpec?: {
|
||||
default?: unknown
|
||||
type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array'
|
||||
type?: FieldSpecType
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
values?: unknown[]
|
||||
@@ -114,7 +116,33 @@ export default class InputSpec extends React.Component<InputSpecProps> {
|
||||
/>
|
||||
}
|
||||
}
|
||||
default: return null
|
||||
case 'numberArray': return (
|
||||
<InputDynamicArray
|
||||
{...commonProps as InputDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type="number"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
/>
|
||||
)
|
||||
case 'colorArray': return (
|
||||
<InputDynamicArray
|
||||
{...commonProps as InputDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type="color"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
/>
|
||||
)
|
||||
case 'padding': return (
|
||||
<InputArray
|
||||
{...commonProps as InputArrayProps}
|
||||
type="number"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
length={4}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ type LayerEditorInternalProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification & {layers: string[]}}
|
||||
vectorLayers: {[key: string]: any}
|
||||
spec: object
|
||||
spec: any
|
||||
onLayerChanged(...args: unknown[]): unknown
|
||||
onLayerIdChange(...args: unknown[]): unknown
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
@@ -367,6 +367,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
<section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label={t("Layer editor")}
|
||||
data-wd-key="layer-editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
|
||||
@@ -26,13 +26,9 @@ function getFieldSpec(spec: any, layerType: LayerSpecification["type"], fieldNam
|
||||
return fieldSpec
|
||||
}
|
||||
|
||||
function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
return 'layout'
|
||||
}
|
||||
function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string): 'paint' | 'layout' {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
return (fieldName in paint) ? 'paint' : 'layout';
|
||||
}
|
||||
|
||||
type PropertyGroupProps = {
|
||||
|
||||
@@ -7,18 +7,18 @@ import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FunctionInputButtonsInternalProps = {
|
||||
fieldSpec?: any
|
||||
onZoomClick?(...args: unknown[]): unknown
|
||||
onDataClick?(...args: unknown[]): unknown
|
||||
onExpressionClick?(...args: unknown[]): unknown
|
||||
onZoomClick?(): void
|
||||
onDataClick?(): void
|
||||
onExpressionClick?(): void
|
||||
onElevationClick?(): void
|
||||
} & WithTranslation;
|
||||
|
||||
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||
|
||||
if (this.props.fieldSpec.expression?.parameters.includes('zoom')) {
|
||||
expressionInputButton = (
|
||||
const expressionInputButton = (
|
||||
<InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onExpressionClick}
|
||||
@@ -30,7 +30,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
</InputButton>
|
||||
);
|
||||
|
||||
makeZoomInputButton = <InputButton
|
||||
const makeZoomInputButton = <InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
title={t("Convert property into a zoom function")}
|
||||
@@ -38,6 +38,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
<MdFunctions />
|
||||
</InputButton>
|
||||
|
||||
let makeDataInputButton;
|
||||
if (this.props.fieldSpec['property-type'] === 'data-driven') {
|
||||
makeDataInputButton = <InputButton
|
||||
className="maputnik-make-data-function"
|
||||
@@ -52,9 +53,18 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
||||
{makeDataInputButton}
|
||||
{makeZoomInputButton}
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
return <div>{expressionInputButton}</div>
|
||||
} else if (this.props.fieldSpec.expression?.parameters.includes('elevation')) {
|
||||
const inputElevationButton = <InputButton
|
||||
className="maputnik-make-elevation-function"
|
||||
onClick={this.props.onElevationClick}
|
||||
title={t("Convert property into a elevation function")}
|
||||
data-wd-key='make-elevation-function'
|
||||
>
|
||||
<MdFunctions />
|
||||
</InputButton>
|
||||
return <div>{inputElevationButton}</div>
|
||||
} else {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import labelFromFieldName from '../libs/label-from-field-name'
|
||||
|
||||
|
||||
type SpecPropertyProps = FieldSpecProps & {
|
||||
onZoomClick(...args: unknown[]): unknown
|
||||
onDataClick(...args: unknown[]): unknown
|
||||
fieldName?: string
|
||||
fieldType?: string
|
||||
fieldSpec?: any
|
||||
value?: any
|
||||
errors?: {[key: string]: {message: string}}
|
||||
onExpressionClick?(...args: unknown[]): unknown
|
||||
onZoomClick(): void
|
||||
onDataClick(): void
|
||||
onExpressionClick?(): void
|
||||
onElevationClick?(): void
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +32,7 @@ export default class SpecProperty extends React.Component<SpecPropertyProps> {
|
||||
onZoomClick={this.props.onZoomClick}
|
||||
onDataClick={this.props.onDataClick}
|
||||
onExpressionClick={this.props.onExpressionClick}
|
||||
onElevationClick={this.props.onElevationClick}
|
||||
/>
|
||||
|
||||
const error = errors![fieldType+"."+fieldName as any] as any;
|
||||
|
||||
Reference in New Issue
Block a user