1 | import {filterObject} from '../util/util';
|
2 |
|
3 | import styleSpec from '../style-spec/reference/latest';
|
4 | import {
|
5 | validateStyle,
|
6 | validateLayoutProperty,
|
7 | validatePaintProperty,
|
8 | emitValidationErrors
|
9 | } from './validate_style';
|
10 | import {Evented} from '../util/evented';
|
11 | import {Layout, Transitionable, Transitioning, Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue} from './properties';
|
12 | import {supportsPropertyExpression} from '../style-spec/util/properties';
|
13 |
|
14 | import type {FeatureState} from '../style-spec/expression';
|
15 | import type {Bucket} from '../data/bucket';
|
16 | import type Point from '@mapbox/point-geometry';
|
17 | import type {FeatureFilter} from '../style-spec/feature_filter';
|
18 | import type {TransitionParameters, PropertyValue} from './properties';
|
19 | import EvaluationParameters from './evaluation_parameters';
|
20 | import type {CrossfadeParameters} from './evaluation_parameters';
|
21 |
|
22 | import type Transform from '../geo/transform';
|
23 | import type {
|
24 | LayerSpecification,
|
25 | FilterSpecification
|
26 | } from '../style-spec/types.g';
|
27 | import type {CustomLayerInterface} from './style_layer/custom_style_layer';
|
28 | import type Map from '../ui/map';
|
29 | import type {StyleSetterOptions} from './style';
|
30 | import {mat4} from 'gl-matrix';
|
31 | import type {VectorTileFeature} from '@mapbox/vector-tile';
|
32 |
|
33 | const TRANSITION_SUFFIX = '-transition';
|
34 |
|
35 | abstract 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 |
|
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 |
|
175 |
|
176 |
|
177 | return isDataDriven || wasDataDriven || isCrossFadedProperty || this._handleOverridablePaintPropertyUpdate(name, oldValue, newValue);
|
178 | }
|
179 | }
|
180 |
|
181 | _handleSpecialPaintPropertyUpdate(_: string) {
|
182 |
|
183 | }
|
184 |
|
185 |
|
186 | _handleOverridablePaintPropertyUpdate<T, R>(name: string, oldValue: PropertyValue<T, R>, newValue: PropertyValue<T, R>): boolean {
|
187 |
|
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 |
|
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 |
|
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 |
|
290 | export default StyleLayer;
|