Skip to content

Commit 6fe9302

Browse files
committed
refactor: move fill outline fallback into fill layer
1 parent f32d290 commit 6fe9302

File tree

3 files changed

+122
-95
lines changed

3 files changed

+122
-95
lines changed

src/data/program_configuration.ts

Lines changed: 14 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import type {
2323
Feature,
2424
FeatureState,
2525
GlobalProperties,
26+
SourceExpression,
2627
CompositeExpression,
27-
FormattedSection,
28-
ZoomConstantExpression,
29-
ZoomDependentExpression
28+
FormattedSection
3029
} from '@maplibre/maplibre-gl-style-spec';
3130
import type {FeatureStates} from '../source/source_state';
3231
import type {DashEntry} from '../render/line_atlas';
@@ -57,20 +56,6 @@ type PaintOptions = {
5756
globalState?: Record<string, any>;
5857
};
5958

60-
type FillColorFallback = PossiblyEvaluatedPropertyValue<Color>['value'] | null;
61-
62-
type SourceExpressionWithRawEvaluation = ZoomConstantExpression<'source'>;
63-
64-
type CompositeExpressionWithRawEvaluation = ZoomDependentExpression<'composite'>;
65-
66-
function getFillColorFallback(layer: TypedStyleLayer, property: string): FillColorFallback {
67-
return property === 'fill-outline-color' ? (layer.paint as any).get('fill-color').value : null;
68-
}
69-
70-
function isNullColorEvaluationError(error: unknown): boolean {
71-
return error instanceof Error && error.message === 'Could not parse color from value \'null\'';
72-
}
73-
7459
/**
7560
* `Binder` is the interface definition for the strategies for constructing,
7661
* uploading, and binding paint property data as GLSL attributes. Most style-
@@ -209,42 +194,37 @@ class CrossFadedConstantBinder implements UniformBinder {
209194
}
210195

211196
class SourceExpressionBinder implements AttributeBinder {
212-
expression: SourceExpressionWithRawEvaluation;
197+
expression: SourceExpression;
213198
type: string;
214-
zoom: number;
215199
maxValue: number;
216-
217-
fillColorFallback: FillColorFallback;
218200
paintVertexArray: StructArray;
219201
paintVertexAttributes: StructArrayMember[];
220202
paintVertexBuffer: VertexBuffer;
221203

222-
constructor(expression: SourceExpressionWithRawEvaluation, names: string[], type: string, zoom: number, fillColorFallback: FillColorFallback, PaintVertexArray: {
204+
constructor(expression: SourceExpression, names: string[], type: string, PaintVertexArray: {
223205
new (...args: any): StructArray;
224206
}) {
225207
this.expression = expression;
226208
this.type = type;
227-
this.zoom = zoom;
228209
this.maxValue = 0;
229210
this.paintVertexAttributes = names.map((name) => ({
230211
name: `a_${name}`,
231212
type: 'Float32',
232213
components: type === 'color' ? 2 : 1,
233214
offset: 0
234215
}));
235-
this.fillColorFallback = fillColorFallback;
236216
this.paintVertexArray = new PaintVertexArray();
237217
}
238218

239219
populatePaintArray(newLength: number, feature: Feature, options: PaintOptions) {
240220
const start = this.paintVertexArray.length;
241-
const value = this._evaluateValue(feature, {}, options, options.canonical, options.formattedSection);
221+
const value = this.expression.evaluate(new EvaluationParameters(0, options), feature, {}, options.canonical, [], options.formattedSection);
242222
this.paintVertexArray.resize(newLength);
243223
this._setPaintValue(start, newLength, value);
244224
}
245225

246226
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
247-
const value = this._evaluateValue(feature, featureState, options);
227+
const value = this.expression.evaluate(new EvaluationParameters(0, options), feature, featureState);
248228
this._setPaintValue(start, end, value);
249229
}
250230

@@ -262,32 +242,6 @@ class SourceExpressionBinder implements AttributeBinder {
262242
}
263243
}
264244

265-
_evaluateFillColorFallback(feature: Feature, featureState: FeatureState, options: PaintOptions, canonical?: CanonicalTileID, formattedSection?: FormattedSection): Color | null | undefined {
266-
if (!this.fillColorFallback) return null;
267-
if (this.fillColorFallback.kind === 'constant') {
268-
return this.fillColorFallback.value;
269-
}
270-
return this.fillColorFallback.evaluate(new EvaluationParameters(this.zoom, options), feature, featureState, canonical, [], formattedSection);
271-
}
272-
273-
_evaluateValue(feature: Feature, featureState: FeatureState, options: PaintOptions, canonical?: CanonicalTileID, formattedSection?: FormattedSection) {
274-
if (this.type !== 'color' || !this.fillColorFallback) {
275-
return this.expression.evaluate(new EvaluationParameters(0, options), feature, featureState, canonical, [], formattedSection);
276-
}
277-
278-
try {
279-
const value = this.expression.evaluateWithoutErrorHandling(new EvaluationParameters(0, options), feature, featureState, canonical, [], formattedSection);
280-
return value == null ? this._evaluateFillColorFallback(feature, featureState, options, canonical, formattedSection) : value;
281-
} catch (error) {
282-
if (isNullColorEvaluationError(error)) {
283-
return this._evaluateFillColorFallback(feature, featureState, options, canonical, formattedSection);
284-
}
285-
286-
const value = this.expression.evaluate(new EvaluationParameters(0, options), feature, featureState, canonical, [], formattedSection);
287-
return value == null ? this._evaluateFillColorFallback(feature, featureState, options, canonical, formattedSection) : value;
288-
}
289-
}
290-
291245
upload(context: Context) {
292246
if (this.paintVertexArray?.arrayBuffer.byteLength) {
293247
if (this.paintVertexBuffer?.buffer) {
@@ -306,19 +260,18 @@ class SourceExpressionBinder implements AttributeBinder {
306260
}
307261

308262
class CompositeExpressionBinder implements AttributeBinder, UniformBinder {
309-
expression: CompositeExpressionWithRawEvaluation;
263+
expression: CompositeExpression;
310264
uniformNames: string[];
311265
type: string;
312266
useIntegerZoom: boolean;
313267
zoom: number;
314268
maxValue: number;
315269

316-
fillColorFallback: FillColorFallback;
317270
paintVertexArray: StructArray;
318271
paintVertexAttributes: StructArrayMember[];
319272
paintVertexBuffer: VertexBuffer;
320273

321-
constructor(expression: CompositeExpressionWithRawEvaluation, names: string[], type: string, useIntegerZoom: boolean, zoom: number, fillColorFallback: FillColorFallback, PaintVertexArray: {
274+
constructor(expression: CompositeExpression, names: string[], type: string, useIntegerZoom: boolean, zoom: number, PaintVertexArray: {
322275
new (...args: any): StructArray;
323276
}) {
324277
this.expression = expression;
@@ -327,7 +280,6 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder {
327280
this.useIntegerZoom = useIntegerZoom;
328281
this.zoom = zoom;
329282
this.maxValue = 0;
330-
this.fillColorFallback = fillColorFallback;
331283
this.paintVertexAttributes = names.map((name) => ({
332284
name: `a_${name}`,
333285
type: 'Float32',
@@ -337,43 +289,17 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder {
337289
this.paintVertexArray = new PaintVertexArray();
338290
}
339291

340-
_evaluateFillColorFallback(zoom: number, feature: Feature, featureState: FeatureState, options: PaintOptions, canonical?: CanonicalTileID, formattedSection?: FormattedSection): Color | null | undefined {
341-
if (!this.fillColorFallback) return null;
342-
if (this.fillColorFallback.kind === 'constant') {
343-
return this.fillColorFallback.value;
344-
}
345-
return this.fillColorFallback.evaluate(new EvaluationParameters(zoom, options), feature, featureState, canonical, [], formattedSection);
346-
}
347-
348-
_evaluateValue(zoom: number, feature: Feature, featureState: FeatureState, options: PaintOptions, canonical?: CanonicalTileID, formattedSection?: FormattedSection) {
349-
if (this.type !== 'color' || !this.fillColorFallback) {
350-
return this.expression.evaluate(new EvaluationParameters(zoom, options), feature, featureState, canonical, [], formattedSection);
351-
}
352-
353-
try {
354-
const value = this.expression.evaluateWithoutErrorHandling(new EvaluationParameters(zoom, options), feature, featureState, canonical, [], formattedSection);
355-
return value == null ? this._evaluateFillColorFallback(zoom, feature, featureState, options, canonical, formattedSection) : value;
356-
} catch (error) {
357-
if (isNullColorEvaluationError(error)) {
358-
return this._evaluateFillColorFallback(zoom, feature, featureState, options, canonical, formattedSection);
359-
}
360-
361-
const value = this.expression.evaluate(new EvaluationParameters(zoom, options), feature, featureState, canonical, [], formattedSection);
362-
return value == null ? this._evaluateFillColorFallback(zoom, feature, featureState, options, canonical, formattedSection) : value;
363-
}
364-
}
365-
366292
populatePaintArray(newLength: number, feature: Feature, options: PaintOptions) {
367-
const min = this._evaluateValue(this.zoom, feature, {}, options, options.canonical, options.formattedSection);
368-
const max = this._evaluateValue(this.zoom + 1, feature, {}, options, options.canonical, options.formattedSection);
293+
const min = this.expression.evaluate(new EvaluationParameters(this.zoom, options), feature, {}, options.canonical, [], options.formattedSection);
294+
const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1, options), feature, {}, options.canonical, [], options.formattedSection);
369295
const start = this.paintVertexArray.length;
370296
this.paintVertexArray.resize(newLength);
371297
this._setPaintValue(start, newLength, min, max);
372298
}
373299

374300
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
375-
const min = this._evaluateValue(this.zoom, feature, featureState, options);
376-
const max = this._evaluateValue(this.zoom + 1, feature, featureState, options);
301+
const min = this.expression.evaluate(new EvaluationParameters(this.zoom, options), feature, featureState);
302+
const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1, options), feature, featureState);
377303
this._setPaintValue(start, end, min, max);
378304
}
379305

@@ -583,8 +509,6 @@ export class ProgramConfiguration {
583509
const useIntegerZoom = (value.property as any).useIntegerZoom;
584510
const propType = value.property.specification['property-type'];
585511
const isCrossFaded = propType === 'cross-faded' || propType === 'cross-faded-data-driven';
586-
const fillColorFallback = getFillColorFallback(layer, property);
587-
588512
if (expression.kind === 'constant') {
589513
this.binders[property] = isCrossFaded ?
590514
new CrossFadedConstantBinder(expression.value, names) :
@@ -596,11 +520,11 @@ export class ProgramConfiguration {
596520
property === 'line-dasharray' ?
597521
new CrossFadedDasharrayBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
598522
new CrossFadedPatternBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
599-
new SourceExpressionBinder(expression as SourceExpressionWithRawEvaluation, names, type, zoom, fillColorFallback, StructArrayLayout);
523+
new SourceExpressionBinder(expression as SourceExpression, names, type, StructArrayLayout);
600524
keys.push(`/a_${property}`);
601525
} else {
602526
const StructArrayLayout = layoutType(property, type, 'composite');
603-
this.binders[property] = new CompositeExpressionBinder(expression as CompositeExpressionWithRawEvaluation, names, type, useIntegerZoom, zoom, fillColorFallback, StructArrayLayout);
527+
this.binders[property] = new CompositeExpressionBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout);
604528
keys.push(`/z_${property}`);
605529
}
606530
}
@@ -656,9 +580,6 @@ export class ProgramConfiguration {
656580
binder instanceof CrossFadedBinder) && binder.expression.isStateDependent === true) {
657581
//AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255
658582
const value = (layer.paint as any).get(property);
659-
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder) {
660-
binder.fillColorFallback = getFillColorFallback(layer, property);
661-
}
662583
binder.expression = value.value;
663584
binder.updatePaintArray(pos.start, pos.end, feature, featureStates[id], options);
664585
dirty = true;

src/style/style_layer.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {describe, test, expect} from 'vitest';
1+
import {describe, test, expect, vi} from 'vitest';
22
import {createStyleLayer} from './create_style_layer';
33
import {FillStyleLayer} from './style_layer/fill_style_layer';
44
import {extend} from '../util/util';
@@ -175,6 +175,27 @@ describe('StyleLayer.setPaintProperty', () => {
175175

176176
});
177177

178+
test('uses fill-color when fill-outline-color feature-state is missing', () => {
179+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
180+
const layer = createStyleLayer({
181+
id: 'building',
182+
type: 'fill',
183+
source: 'streets',
184+
paint: {
185+
'fill-color': '#00f',
186+
'fill-outline-color': ['feature-state', 'outline-color']
187+
}
188+
}, {}) as FillStyleLayer;
189+
190+
layer.recalculate({zoom: 0, zoomHistory: {}} as EvaluationParameters, undefined);
191+
192+
const outlineColor = layer.paint.get('fill-outline-color').evaluate({type: 'Polygon', properties: {}}, {});
193+
194+
expect(outlineColor).toEqual(new Color(0, 0, 1, 1));
195+
expect(warn).not.toHaveBeenCalled();
196+
warn.mockRestore();
197+
});
198+
178199
test('sets null property value', () => {
179200
const layer = createStyleLayer({
180201
'id': 'background',

src/style/style_layer/fill_style_layer.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,91 @@ import {FillBucket} from '../../data/bucket/fill_bucket';
33
import {polygonIntersectsMultiPolygon} from '../../util/intersection_tests';
44
import {translateDistance, translate} from '../query_utils';
55
import properties, {type FillLayoutPropsPossiblyEvaluated, type FillPaintPropsPossiblyEvaluated} from './fill_style_layer_properties.g';
6+
import {PossiblyEvaluatedPropertyValue} from '../properties';
67

78
import type {Transitionable, Transitioning, Layout, PossiblyEvaluated} from '../properties';
8-
import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
9+
import type {LayerSpecification, Color, Feature, FeatureState, GlobalProperties, SourceExpression, CompositeExpression, FormattedSection} from '@maplibre/maplibre-gl-style-spec';
910
import type {BucketParameters} from '../../data/bucket';
1011
import type {FillLayoutProps, FillPaintProps} from './fill_style_layer_properties.g';
1112
import type {EvaluationParameters} from '../evaluation_parameters';
13+
import type {CanonicalTileID} from '../../tile/tile_id';
14+
15+
type FillColorExpression = SourceExpression | CompositeExpression;
16+
17+
function isNullColorEvaluationError(error: unknown): boolean {
18+
return error instanceof Error && error.message === 'Could not parse color from value \'null\'';
19+
}
20+
21+
function evaluateColorExpression(
22+
expression: PossiblyEvaluatedPropertyValue<Color>['value'],
23+
globals: GlobalProperties,
24+
feature?: Feature,
25+
featureState?: FeatureState,
26+
canonical?: CanonicalTileID,
27+
availableImages?: string[],
28+
formattedSection?: FormattedSection,
29+
raw?: boolean,
30+
): Color | null | undefined {
31+
if (expression.kind === 'constant') {
32+
return expression.value;
33+
}
34+
35+
if (raw && 'evaluateWithoutErrorHandling' in expression) {
36+
return expression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
37+
}
38+
39+
return expression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
40+
}
41+
42+
function createOutlineColorExpression(outline: PossiblyEvaluatedPropertyValue<Color>, fill: PossiblyEvaluatedPropertyValue<Color>): FillColorExpression {
43+
const outlineValue = outline.value;
44+
const fillValue = fill.value;
45+
const isComposite = outlineValue.kind === 'composite' || fillValue.kind === 'composite';
46+
const compositeTemplate = outlineValue.kind === 'composite' ? outlineValue : fillValue.kind === 'composite' ? fillValue : null;
47+
48+
const evaluate = (
49+
globals: GlobalProperties,
50+
feature?: Feature,
51+
featureState?: FeatureState,
52+
canonical?: CanonicalTileID,
53+
availableImages?: string[],
54+
formattedSection?: FormattedSection,
55+
): Color | null | undefined => {
56+
try {
57+
const value = evaluateColorExpression(outlineValue, globals, feature, featureState, canonical, availableImages, formattedSection, true);
58+
if (value != null) return value;
59+
} catch (error) {
60+
if (!isNullColorEvaluationError(error)) {
61+
const warnedValue = evaluateColorExpression(outlineValue, globals, feature, featureState, canonical, availableImages, formattedSection);
62+
if (warnedValue != null) return warnedValue;
63+
return evaluateColorExpression(fillValue, globals, feature, featureState, canonical, availableImages, formattedSection);
64+
}
65+
}
66+
67+
return evaluateColorExpression(fillValue, globals, feature, featureState, canonical, availableImages, formattedSection);
68+
};
69+
70+
if (isComposite) {
71+
return {
72+
kind: 'composite',
73+
zoomStops: compositeTemplate.zoomStops,
74+
interpolationType: compositeTemplate.interpolationType,
75+
interpolationFactor: compositeTemplate.interpolationFactor.bind(compositeTemplate),
76+
isStateDependent: (outlineValue.kind !== 'constant' && outlineValue.isStateDependent) || (fillValue.kind !== 'constant' && fillValue.isStateDependent),
77+
globalStateRefs: new Set([...(outlineValue.kind === 'constant' ? [] : outlineValue.globalStateRefs), ...(fillValue.kind === 'constant' ? [] : fillValue.globalStateRefs)]),
78+
_globalState: compositeTemplate._globalState,
79+
evaluate,
80+
};
81+
}
82+
83+
return {
84+
kind: 'source',
85+
isStateDependent: (outlineValue.kind !== 'constant' && outlineValue.isStateDependent) || (fillValue.kind !== 'constant' && fillValue.isStateDependent),
86+
globalStateRefs: new Set([...(outlineValue.kind === 'constant' ? [] : outlineValue.globalStateRefs), ...(fillValue.kind === 'constant' ? [] : fillValue.globalStateRefs)]),
87+
_globalState: outlineValue.kind !== 'constant' ? outlineValue._globalState : fillValue.kind !== 'constant' ? fillValue._globalState : null,
88+
evaluate,
89+
};
90+
}
1291

1392
export const isFillStyleLayer = (layer: StyleLayer): layer is FillStyleLayer => layer.type === 'fill';
1493

@@ -30,6 +109,12 @@ export class FillStyleLayer extends StyleLayer {
30109
const outlineColor = this.paint._values['fill-outline-color'];
31110
if (outlineColor.value.kind === 'constant' && outlineColor.value.value === undefined) {
32111
this.paint._values['fill-outline-color'] = this.paint._values['fill-color'];
112+
} else if (outlineColor.value.kind === 'source' || outlineColor.value.kind === 'composite') {
113+
this.paint._values['fill-outline-color'] = new PossiblyEvaluatedPropertyValue(
114+
outlineColor.property,
115+
createOutlineColorExpression(outlineColor, this.paint._values['fill-color']),
116+
outlineColor.parameters,
117+
);
33118
}
34119
}
35120

0 commit comments

Comments
 (0)