UNPKG

10.1 kBPlain TextView Raw
1import {filterObject} from '../util/util';
2
3import styleSpec from '../style-spec/reference/latest';
4import {
5 validateStyle,
6 validateLayoutProperty,
7 validatePaintProperty,
8 emitValidationErrors
9} from './validate_style';
10import {Evented} from '../util/evented';
11import {Layout, Transitionable, Transitioning, Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue} from './properties';
12import {supportsPropertyExpression} from '../style-spec/util/properties';
13
14import type {FeatureState} from '../style-spec/expression';
15import type {Bucket} from '../data/bucket';
16import type Point from '@mapbox/point-geometry';
17import type {FeatureFilter} from '../style-spec/feature_filter';
18import type {TransitionParameters, PropertyValue} from './properties';
19import EvaluationParameters from './evaluation_parameters';
20import type {CrossfadeParameters} from './evaluation_parameters';
21
22import type Transform from '../geo/transform';
23import type {
24 LayerSpecification,
25 FilterSpecification
26} from '../style-spec/types.g';
27import type {CustomLayerInterface} from './style_layer/custom_style_layer';
28import type Map from '../ui/map';
29import type {StyleSetterOptions} from './style';
30import {mat4} from 'gl-matrix';
31import type {VectorTileFeature} from '@mapbox/vector-tile';
32
33const TRANSITION_SUFFIX = '-transition';
34
35abstract class StyleLayer extends Evented {
36 id: string;
37 metadata: unknown;
38 type: LayerSpecification['type'] | CustomLayerInterface['type'];
39 source: string;
40 sourceLayer: string;
41 minzoom: number;
42 maxzoom: number;
43 filter: FilterSpecification | void;
44 visibility: 'visible' | 'none' | void;
45 _crossfadeParameters: CrossfadeParameters;
46
47 _unevaluatedLayout: Layout<any>;
48 readonly layout: unknown;
49
50 _transitionablePaint: Transitionable<any>;
51 _transitioningPaint: Transitioning<any>;
52 readonly paint: unknown;
53
54 _featureFilter: FeatureFilter;
55
56 readonly onAdd: ((map: Map) => void);
57 readonly onRemove: ((map: Map) => void);
58
59 queryRadius?(bucket: Bucket): number;
60 queryIntersectsFeature?(
61 queryGeometry: Array<Point>,
62 feature: VectorTileFeature,
63 featureState: FeatureState,
64 geometry: Array<Array<Point>>,
65 zoom: number,
66 transform: Transform,
67 pixelsToTileUnits: number,
68 pixelPosMatrix: mat4
69 ): boolean | number;
70
71 constructor(layer: LayerSpecification | CustomLayerInterface, properties: Readonly<{
72 layout?: Properties<any>;
73 paint?: Properties<any>;
74 }>) {
75 super();
76
77 this.id = layer.id;
78 this.type = layer.type;
79 this._featureFilter = {filter: () => true, needGeometry: false};
80
81 if (layer.type === 'custom') return;
82
83 layer = (layer as any as LayerSpecification);
84
85 this.metadata = layer.metadata;
86 this.minzoom = layer.minzoom;
87 this.maxzoom = layer.maxzoom;
88
89 if (layer.type !== 'background') {
90 this.source = layer.source;
91 this.sourceLayer = layer['source-layer'];
92 this.filter = layer.filter;
93 }
94
95 if (properties.layout) {
96 this._unevaluatedLayout = new Layout(properties.layout);
97 }
98
99 if (properties.paint) {
100 this._transitionablePaint = new Transitionable(properties.paint);
101
102 for (const property in layer.paint) {
103 this.setPaintProperty(property, layer.paint[property], {validate: false});
104 }
105 for (const property in layer.layout) {
106 this.setLayoutProperty(property, layer.layout[property], {validate: false});
107 }
108
109 this._transitioningPaint = this._transitionablePaint.untransitioned();
110 //$FlowFixMe
111 this.paint = new PossiblyEvaluated(properties.paint);
112 }
113 }
114
115 getCrossfadeParameters() {
116 return this._crossfadeParameters;
117 }
118
119 getLayoutProperty(name: string) {
120 if (name === 'visibility') {
121 return this.visibility;
122 }
123
124 return this._unevaluatedLayout.getValue(name);
125 }
126
127 setLayoutProperty(name: string, value: any, options: StyleSetterOptions = {}) {
128 if (value !== null && value !== undefined) {
129 const key = `layers.${this.id}.layout.${name}`;
130 if (this._validate(validateLayoutProperty, key, name, value, options)) {
131 return;
132 }
133 }
134
135 if (name === 'visibility') {
136 this.visibility = value;
137 return;
138 }
139
140 this._unevaluatedLayout.setValue(name, value);
141 }
142
143 getPaintProperty(name: string) {
144 if (name.endsWith(TRANSITION_SUFFIX)) {
145 return this._transitionablePaint.getTransition(name.slice(0, -TRANSITION_SUFFIX.length));
146 } else {
147 return this._transitionablePaint.getValue(name);
148 }
149 }
150
151 setPaintProperty(name: string, value: unknown, options: StyleSetterOptions = {}) {
152 if (value !== null && value !== undefined) {
153 const key = `layers.${this.id}.paint.${name}`;
154 if (this._validate(validatePaintProperty, key, name, value, options)) {
155 return false;
156 }
157 }
158
159 if (name.endsWith(TRANSITION_SUFFIX)) {
160 this._transitionablePaint.setTransition(name.slice(0, -TRANSITION_SUFFIX.length), (value as any) || undefined);
161 return false;
162 } else {
163 const transitionable = this._transitionablePaint._values[name];
164 const isCrossFadedProperty = transitionable.property.specification['property-type'] === 'cross-faded-data-driven';
165 const wasDataDriven = transitionable.value.isDataDriven();
166 const oldValue = transitionable.value;
167
168 this._transitionablePaint.setValue(name, value);
169 this._handleSpecialPaintPropertyUpdate(name);
170
171 const newValue = this._transitionablePaint._values[name].value;
172 const isDataDriven = newValue.isDataDriven();
173
174 // if a cross-faded value is changed, we need to make sure the new icons get added to each tile's iconAtlas
175 // so a call to _updateLayer is necessary, and we return true from this function so it gets called in
176 // Style#setPaintProperty
177 return isDataDriven || wasDataDriven || isCrossFadedProperty || this._handleOverridablePaintPropertyUpdate(name, oldValue, newValue);
178 }
179 }
180
181 _handleSpecialPaintPropertyUpdate(_: string) {
182 // No-op; can be overridden by derived classes.
183 }
184
185 // eslint-disable-next-line @typescript-eslint/no-unused-vars
186 _handleOverridablePaintPropertyUpdate<T, R>(name: string, oldValue: PropertyValue<T, R>, newValue: PropertyValue<T, R>): boolean {
187 // No-op; can be overridden by derived classes.
188 return false;
189 }
190
191 isHidden(zoom: number) {
192 if (this.minzoom && zoom < this.minzoom) return true;
193 if (this.maxzoom && zoom >= this.maxzoom) return true;
194 return this.visibility === 'none';
195 }
196
197 updateTransitions(parameters: TransitionParameters) {
198 this._transitioningPaint = this._transitionablePaint.transitioned(parameters, this._transitioningPaint);
199 }
200
201 hasTransition() {
202 return this._transitioningPaint.hasTransition();
203 }
204
205 recalculate(parameters: EvaluationParameters, availableImages: Array<string>) {
206 if (parameters.getCrossfadeParameters) {
207 this._crossfadeParameters = parameters.getCrossfadeParameters();
208 }
209
210 if (this._unevaluatedLayout) {
211 (this as any).layout = this._unevaluatedLayout.possiblyEvaluate(parameters, undefined, availableImages);
212 }
213
214 (this as any).paint = this._transitioningPaint.possiblyEvaluate(parameters, undefined, availableImages);
215 }
216
217 serialize(): LayerSpecification {
218 const output: LayerSpecification = {
219 'id': this.id,
220 'type': this.type as LayerSpecification['type'],
221 'source': this.source,
222 'source-layer': this.sourceLayer,
223 'metadata': this.metadata,
224 'minzoom': this.minzoom,
225 'maxzoom': this.maxzoom,
226 'filter': this.filter as FilterSpecification,
227 'layout': this._unevaluatedLayout && this._unevaluatedLayout.serialize(),
228 'paint': this._transitionablePaint && this._transitionablePaint.serialize()
229 };
230
231 if (this.visibility) {
232 output.layout = output.layout || {};
233 output.layout.visibility = this.visibility;
234 }
235
236 return filterObject(output, (value, key) => {
237 return value !== undefined &&
238 !(key === 'layout' && !Object.keys(value).length) &&
239 !(key === 'paint' && !Object.keys(value).length);
240 });
241 }
242
243 _validate(validate: Function, key: string, name: string, value: unknown, options: StyleSetterOptions = {}) {
244 if (options && options.validate === false) {
245 return false;
246 }
247 return emitValidationErrors(this, validate.call(validateStyle, {
248 key,
249 layerType: this.type,
250 objectKey: name,
251 value,
252 styleSpec,
253 // Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407
254 style: {glyphs: true, sprite: true}
255 }));
256 }
257
258 is3D() {
259 return false;
260 }
261
262 isTileClipped() {
263 return false;
264 }
265
266 hasOffscreenPass() {
267 return false;
268 }
269
270 resize() {
271 // noop
272 }
273
274 isStateDependent() {
275 for (const property in (this as any).paint._values) {
276 const value = (this as any).paint.get(property);
277 if (!(value instanceof PossiblyEvaluatedPropertyValue) || !supportsPropertyExpression(value.property.specification)) {
278 continue;
279 }
280
281 if ((value.value.kind === 'source' || value.value.kind === 'composite') &&
282 value.value.isStateDependent) {
283 return true;
284 }
285 }
286 return false;
287 }
288}
289
290export default StyleLayer;