@deck.gl-community/basemap-layers 9.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +14 -0
- package/dist/atmosphere-layer.d.ts +11 -0
- package/dist/atmosphere-layer.d.ts.map +1 -0
- package/dist/atmosphere-layer.js +15 -0
- package/dist/atmosphere-layer.js.map +1 -0
- package/dist/basemap-layer.d.ts +67 -0
- package/dist/basemap-layer.d.ts.map +1 -0
- package/dist/basemap-layer.js +115 -0
- package/dist/basemap-layer.js.map +1 -0
- package/dist/globe-layers.d.ts +22 -0
- package/dist/globe-layers.d.ts.map +1 -0
- package/dist/globe-layers.js +451 -0
- package/dist/globe-layers.js.map +1 -0
- package/dist/index.cjs +947 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/map-style-loader.d.ts +49 -0
- package/dist/map-style-loader.d.ts.map +1 -0
- package/dist/map-style-loader.js +35 -0
- package/dist/map-style-loader.js.map +1 -0
- package/dist/map-style-schema.d.ts +71 -0
- package/dist/map-style-schema.d.ts.map +1 -0
- package/dist/map-style-schema.js +45 -0
- package/dist/map-style-schema.js.map +1 -0
- package/dist/map-style.cjs +250 -0
- package/dist/map-style.cjs.map +7 -0
- package/dist/map-style.d.ts +7 -0
- package/dist/map-style.d.ts.map +1 -0
- package/dist/map-style.js +6 -0
- package/dist/map-style.js.map +1 -0
- package/dist/mapbox-style.d.ts +41 -0
- package/dist/mapbox-style.d.ts.map +1 -0
- package/dist/mapbox-style.js +113 -0
- package/dist/mapbox-style.js.map +1 -0
- package/dist/mvt-label-layer.d.ts +8142 -0
- package/dist/mvt-label-layer.d.ts.map +1 -0
- package/dist/mvt-label-layer.js +175 -0
- package/dist/mvt-label-layer.js.map +1 -0
- package/dist/style-resolver.d.ts +88 -0
- package/dist/style-resolver.d.ts.map +1 -0
- package/dist/style-resolver.js +63 -0
- package/dist/style-resolver.js.map +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +18 -0
- package/dist/util.js.map +1 -0
- package/package.json +60 -0
- package/src/atmosphere-layer.ts +15 -0
- package/src/basemap-layer.ts +183 -0
- package/src/globe-layers.ts +780 -0
- package/src/index.ts +3 -0
- package/src/map-style-loader.ts +52 -0
- package/src/map-style-schema.ts +54 -0
- package/src/map-style.ts +18 -0
- package/src/mapbox-style.ts +196 -0
- package/src/mvt-label-layer.ts +269 -0
- package/src/style-resolver.ts +173 -0
- package/src/util.ts +38 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {LoaderContext, LoaderOptions, LoaderWithParser} from '@loaders.gl/loader-utils';
|
|
2
|
+
import {ResolvedBasemapStyleSchema} from './map-style-schema';
|
|
3
|
+
import {resolveBasemapStyle} from './style-resolver';
|
|
4
|
+
import type {BasemapLoadOptions, BasemapStyle, ResolvedBasemapStyle} from './style-resolver';
|
|
5
|
+
|
|
6
|
+
// __VERSION__ is injected by babel-plugin-version-inline
|
|
7
|
+
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
|
|
8
|
+
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
|
|
9
|
+
|
|
10
|
+
/** Namespaced loaders.gl options for {@link MapStyleLoader}. */
|
|
11
|
+
export type MapStyleLoaderOptions = LoaderOptions & {
|
|
12
|
+
mapStyle?: NonNullable<BasemapLoadOptions>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getMapStyleLoadOptions(
|
|
16
|
+
options?: MapStyleLoaderOptions,
|
|
17
|
+
context?: LoaderContext
|
|
18
|
+
): NonNullable<BasemapLoadOptions> {
|
|
19
|
+
return {
|
|
20
|
+
...options?.mapStyle,
|
|
21
|
+
baseUrl: options?.mapStyle?.baseUrl || context?.url || context?.baseUrl,
|
|
22
|
+
fetch: options?.mapStyle?.fetch || (context?.fetch as typeof fetch | undefined)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** loaders.gl-compatible loader that resolves and validates map-style documents. */
|
|
27
|
+
export const MapStyleLoader = {
|
|
28
|
+
dataType: null as unknown as ResolvedBasemapStyle,
|
|
29
|
+
batchType: null as never,
|
|
30
|
+
|
|
31
|
+
name: 'Map Style',
|
|
32
|
+
id: 'map-style',
|
|
33
|
+
module: 'basemap-layers',
|
|
34
|
+
version: VERSION,
|
|
35
|
+
worker: false,
|
|
36
|
+
extensions: ['json'],
|
|
37
|
+
mimeTypes: ['application/json', 'application/vnd.mapbox.style+json'],
|
|
38
|
+
text: true,
|
|
39
|
+
options: {
|
|
40
|
+
mapStyle: {}
|
|
41
|
+
},
|
|
42
|
+
parse: async (
|
|
43
|
+
data: ArrayBuffer | string,
|
|
44
|
+
options?: MapStyleLoaderOptions,
|
|
45
|
+
context?: LoaderContext
|
|
46
|
+
) => {
|
|
47
|
+
const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
|
48
|
+
const style = JSON.parse(text) as BasemapStyle;
|
|
49
|
+
const resolved = await resolveBasemapStyle(style, getMapStyleLoadOptions(options, context));
|
|
50
|
+
return ResolvedBasemapStyleSchema.parse(resolved);
|
|
51
|
+
}
|
|
52
|
+
} as const satisfies LoaderWithParser<ResolvedBasemapStyle, never, MapStyleLoaderOptions>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
BasemapSource,
|
|
4
|
+
BasemapStyle,
|
|
5
|
+
BasemapStyleLayer,
|
|
6
|
+
ResolvedBasemapStyle
|
|
7
|
+
} from './style-resolver';
|
|
8
|
+
|
|
9
|
+
/** Zod schema for a basemap source entry. */
|
|
10
|
+
export const BasemapSourceSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
type: z.string().optional(),
|
|
13
|
+
url: z.string().optional(),
|
|
14
|
+
tiles: z.array(z.string()).optional(),
|
|
15
|
+
minzoom: z.number().optional(),
|
|
16
|
+
maxzoom: z.number().optional(),
|
|
17
|
+
tileSize: z.number().optional()
|
|
18
|
+
})
|
|
19
|
+
.catchall(z.unknown()) satisfies z.ZodType<BasemapSource>;
|
|
20
|
+
|
|
21
|
+
/** Zod schema for a basemap style layer entry. */
|
|
22
|
+
export const BasemapStyleLayerSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
type: z.string(),
|
|
26
|
+
source: z.string().optional(),
|
|
27
|
+
'source-layer': z.string().optional(),
|
|
28
|
+
minzoom: z.number().optional(),
|
|
29
|
+
maxzoom: z.number().optional(),
|
|
30
|
+
filter: z.array(z.unknown()).optional(),
|
|
31
|
+
paint: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
layout: z.record(z.string(), z.unknown()).optional()
|
|
33
|
+
})
|
|
34
|
+
.catchall(z.unknown()) satisfies z.ZodType<BasemapStyleLayer>;
|
|
35
|
+
|
|
36
|
+
/** Zod schema for a MapLibre / Mapbox style document. */
|
|
37
|
+
export const BasemapStyleSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
version: z.number().optional(),
|
|
40
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
41
|
+
sources: z.record(z.string(), BasemapSourceSchema).optional(),
|
|
42
|
+
layers: z.array(BasemapStyleLayerSchema).optional()
|
|
43
|
+
})
|
|
44
|
+
.catchall(z.unknown()) satisfies z.ZodType<BasemapStyle>;
|
|
45
|
+
|
|
46
|
+
/** Zod schema for a fully resolved basemap style document. */
|
|
47
|
+
export const ResolvedBasemapStyleSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
version: z.number().optional(),
|
|
50
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
51
|
+
sources: z.record(z.string(), BasemapSourceSchema),
|
|
52
|
+
layers: z.array(BasemapStyleLayerSchema)
|
|
53
|
+
})
|
|
54
|
+
.catchall(z.unknown()) satisfies z.ZodType<ResolvedBasemapStyle>;
|
package/src/map-style.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Map-style helpers exported from `@deck.gl-community/basemap-layers/map-style`. */
|
|
2
|
+
export {filterFeatures, findFeaturesStyledByLayer, parseProperties} from './mapbox-style';
|
|
3
|
+
export {
|
|
4
|
+
BasemapSourceSchema,
|
|
5
|
+
BasemapStyleLayerSchema,
|
|
6
|
+
BasemapStyleSchema,
|
|
7
|
+
ResolvedBasemapStyleSchema
|
|
8
|
+
} from './map-style-schema';
|
|
9
|
+
export {MapStyleLoader} from './map-style-loader';
|
|
10
|
+
export type {MapStyleLoaderOptions} from './map-style-loader';
|
|
11
|
+
export {
|
|
12
|
+
resolveBasemapStyle,
|
|
13
|
+
type BasemapLoadOptions,
|
|
14
|
+
type BasemapSource,
|
|
15
|
+
type BasemapStyle,
|
|
16
|
+
type BasemapStyleLayer,
|
|
17
|
+
type ResolvedBasemapStyle
|
|
18
|
+
} from './style-resolver';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {Color, expression, featureFilter, latest as Reference} from '@mapbox/mapbox-gl-style-spec';
|
|
2
|
+
|
|
3
|
+
type GlobalProperties = {
|
|
4
|
+
zoom?: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type GeometryLike = {
|
|
9
|
+
type: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type FeatureLike = {
|
|
13
|
+
type?: number | string;
|
|
14
|
+
geometry?: GeometryLike;
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type FilterFeaturesOptions = {
|
|
19
|
+
features: FeatureLike[];
|
|
20
|
+
filter: unknown[];
|
|
21
|
+
globalProperties?: GlobalProperties;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type FindFeaturesStyledByLayerOptions = {
|
|
25
|
+
features: Record<string, Record<string, FeatureLike[]>>;
|
|
26
|
+
layer: {
|
|
27
|
+
source?: string;
|
|
28
|
+
'source-layer'?: string;
|
|
29
|
+
filter?: unknown[];
|
|
30
|
+
};
|
|
31
|
+
globalProperties?: GlobalProperties;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type PropertyReference = Record<string, unknown> | null;
|
|
35
|
+
|
|
36
|
+
type VisitedProperty = {
|
|
37
|
+
layer: Record<string, any>;
|
|
38
|
+
path: string[];
|
|
39
|
+
key: string;
|
|
40
|
+
value: unknown;
|
|
41
|
+
reference: PropertyReference;
|
|
42
|
+
set: (value: unknown) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type VisitOptions = {
|
|
46
|
+
paint?: boolean;
|
|
47
|
+
layout?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const GEOM_TYPES: Record<string, number> = {
|
|
51
|
+
Point: 1,
|
|
52
|
+
MultiPoint: 1,
|
|
53
|
+
LineString: 2,
|
|
54
|
+
MultiLineString: 2,
|
|
55
|
+
Polygon: 3,
|
|
56
|
+
MultiPolygon: 3
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Applies a Mapbox style-spec filter expression to a set of features.
|
|
61
|
+
*/
|
|
62
|
+
export function filterFeatures({
|
|
63
|
+
features,
|
|
64
|
+
filter,
|
|
65
|
+
globalProperties = {}
|
|
66
|
+
}: FilterFeaturesOptions): FeatureLike[] {
|
|
67
|
+
if (!features || features.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const filterFn = featureFilter(filter).filter;
|
|
72
|
+
|
|
73
|
+
return features.filter((feature) => {
|
|
74
|
+
if (![1, 2, 3].includes(Number(feature.type))) {
|
|
75
|
+
feature.type = GEOM_TYPES[feature.geometry?.type || ''] ?? feature.type;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return filterFn(globalProperties, feature);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Finds the source-layer features that participate in a particular style layer
|
|
84
|
+
* and applies the layer's filter expression when present.
|
|
85
|
+
*/
|
|
86
|
+
export function findFeaturesStyledByLayer({
|
|
87
|
+
features,
|
|
88
|
+
layer,
|
|
89
|
+
globalProperties
|
|
90
|
+
}: FindFeaturesStyledByLayerOptions): FeatureLike[] {
|
|
91
|
+
const sourceLayerFeatures = features[layer.source || '']?.[layer['source-layer'] || ''];
|
|
92
|
+
if (!sourceLayerFeatures) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (layer.filter && layer.filter.length > 0) {
|
|
97
|
+
return filterFeatures({
|
|
98
|
+
features: sourceLayerFeatures,
|
|
99
|
+
filter: layer.filter,
|
|
100
|
+
globalProperties
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluates paint properties for a style layer at the requested zoom.
|
|
109
|
+
*/
|
|
110
|
+
export function parseProperties(
|
|
111
|
+
layer: Record<string, any>,
|
|
112
|
+
globalProperties: GlobalProperties
|
|
113
|
+
): Array<Record<string, unknown>> {
|
|
114
|
+
const layerProperties: Array<Record<string, unknown>> = [];
|
|
115
|
+
visitProperties(layer, {paint: true}, (property) => {
|
|
116
|
+
layerProperties.push(parseProperty(property, globalProperties));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return layerProperties;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Walks layout and paint properties for a style layer.
|
|
124
|
+
*/
|
|
125
|
+
function visitProperties(
|
|
126
|
+
layer: Record<string, any>,
|
|
127
|
+
options: VisitOptions,
|
|
128
|
+
callback: (property: VisitedProperty) => void
|
|
129
|
+
): void {
|
|
130
|
+
function inner(targetLayer: Record<string, any>, propertyType: 'paint' | 'layout') {
|
|
131
|
+
const properties = targetLayer[propertyType];
|
|
132
|
+
if (!properties) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Object.keys(properties).forEach((key) => {
|
|
137
|
+
callback({
|
|
138
|
+
layer: targetLayer,
|
|
139
|
+
path: [targetLayer.id, propertyType, key],
|
|
140
|
+
key,
|
|
141
|
+
value: properties[key],
|
|
142
|
+
reference: getPropertyReference(key),
|
|
143
|
+
set(value) {
|
|
144
|
+
properties[key] = value;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.paint) {
|
|
151
|
+
inner(layer, 'paint');
|
|
152
|
+
}
|
|
153
|
+
if (options.layout) {
|
|
154
|
+
inner(layer, 'layout');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the style-spec reference metadata for a property name.
|
|
160
|
+
*/
|
|
161
|
+
function getPropertyReference(propertyName: string): PropertyReference {
|
|
162
|
+
for (let i = 0; i < Reference.layout.length; i++) {
|
|
163
|
+
for (const key in Reference[Reference.layout[i]]) {
|
|
164
|
+
if (key === propertyName) {
|
|
165
|
+
return Reference[Reference.layout[i]][key] as PropertyReference;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < Reference.paint.length; i++) {
|
|
171
|
+
for (const key in Reference[Reference.paint[i]]) {
|
|
172
|
+
if (key === propertyName) {
|
|
173
|
+
return Reference[Reference.paint[i]][key] as PropertyReference;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Evaluates a single style property expression.
|
|
183
|
+
*/
|
|
184
|
+
function parseProperty(
|
|
185
|
+
property: VisitedProperty,
|
|
186
|
+
globalProperties: GlobalProperties
|
|
187
|
+
): Record<string, unknown> {
|
|
188
|
+
const exp = expression.normalizePropertyExpression(property.value, property.reference as any);
|
|
189
|
+
const result = exp.evaluate(globalProperties);
|
|
190
|
+
|
|
191
|
+
if (result instanceof Color) {
|
|
192
|
+
return {[property.key]: result.toArray()};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {[property.key]: result};
|
|
196
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import {CompositeLayer} from '@deck.gl/core';
|
|
2
|
+
import type {UpdateParameters} from '@deck.gl/core';
|
|
3
|
+
import {CollisionFilterExtension} from '@deck.gl/extensions';
|
|
4
|
+
import {GeoJsonLayer, TextLayer} from '@deck.gl/layers';
|
|
5
|
+
|
|
6
|
+
type GeometryType = 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | string;
|
|
7
|
+
|
|
8
|
+
type FeatureGeometry = {
|
|
9
|
+
type: GeometryType;
|
|
10
|
+
coordinates: any;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type FeatureLike = {
|
|
14
|
+
geometry: FeatureGeometry;
|
|
15
|
+
properties?: Record<string, any>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type LabelRow = {
|
|
19
|
+
position: number[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type StyleLayerLike = {
|
|
23
|
+
layout?: Record<string, any>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LabelConfig = {
|
|
27
|
+
labels?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Props accepted by {@link MVTLabelLayer}.
|
|
32
|
+
*/
|
|
33
|
+
export type MVTLabelLayerProps = {
|
|
34
|
+
/** Decoded vector-tile features or a feature-collection-like object. */
|
|
35
|
+
data?: {features?: FeatureLike[]} | FeatureLike[];
|
|
36
|
+
/** Label rendering enablement for the current basemap mode. */
|
|
37
|
+
config: LabelConfig;
|
|
38
|
+
/** Style layer that contributes label rules. */
|
|
39
|
+
styleLayer?: StyleLayerLike;
|
|
40
|
+
/** Zoom level used to resolve stop-based style values. */
|
|
41
|
+
zoom?: number;
|
|
42
|
+
/** Text fill color. */
|
|
43
|
+
textColor?: number[];
|
|
44
|
+
/** Optional text halo/background color. */
|
|
45
|
+
labelBackground?: number[] | null;
|
|
46
|
+
/** Text size units forwarded to `TextLayer`. */
|
|
47
|
+
labelSizeUnits?: 'pixels' | 'meters' | 'common';
|
|
48
|
+
/** Font family used by `TextLayer`. */
|
|
49
|
+
fontFamily?: string;
|
|
50
|
+
/** Enables billboard rendering in the text sublayer. */
|
|
51
|
+
billboard?: boolean;
|
|
52
|
+
/** When `true`, renders the source geometries for debugging. */
|
|
53
|
+
renderGeometry?: boolean;
|
|
54
|
+
/** Additional extension instances passed through to the text sublayer. */
|
|
55
|
+
extensions?: any[];
|
|
56
|
+
/** Active basemap mode. */
|
|
57
|
+
mode?: 'map' | 'globe';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type MVTLabelLayerState = {
|
|
61
|
+
/** Flattened label rows generated from the current tile data. */
|
|
62
|
+
labelData?: LabelRow[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Evaluates a style value that may contain stop definitions.
|
|
67
|
+
*/
|
|
68
|
+
function evaluateStyleValue(value: unknown, zoom: number): unknown {
|
|
69
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (value && typeof value === 'object' && Array.isArray((value as {stops?: unknown[]}).stops)) {
|
|
74
|
+
let resolved = (value as {stops: [number, unknown][]}).stops[0]?.[1];
|
|
75
|
+
for (const stop of (value as {stops: [number, unknown][]}).stops) {
|
|
76
|
+
if (zoom >= stop[0]) {
|
|
77
|
+
resolved = stop[1];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return resolved;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Replaces style-spec token placeholders in a label template.
|
|
88
|
+
*/
|
|
89
|
+
function resolveTokenString(template: unknown, properties?: Record<string, any>): string | null {
|
|
90
|
+
if (typeof template !== 'string') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return template.replace(/\{([^}]+)\}/g, (_, token) => {
|
|
95
|
+
const value = properties?.[token];
|
|
96
|
+
return value === null || value === undefined ? '' : String(value);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns a midpoint for a line geometry.
|
|
102
|
+
*/
|
|
103
|
+
function getLineMidpoint(coordinates: number[][]): number[] | null {
|
|
104
|
+
if (!Array.isArray(coordinates) || coordinates.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return coordinates[Math.floor(coordinates.length / 2)] || coordinates[0] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns a coarse collision priority for a label feature.
|
|
113
|
+
*/
|
|
114
|
+
function getCollisionPriority(feature: FeatureLike): number {
|
|
115
|
+
const properties = feature?.properties || {};
|
|
116
|
+
|
|
117
|
+
if (properties.capital > 0 || properties.class === 'country') {
|
|
118
|
+
return 1000;
|
|
119
|
+
}
|
|
120
|
+
if (properties.class === 'state' || properties.class === 'city') {
|
|
121
|
+
return 750;
|
|
122
|
+
}
|
|
123
|
+
if (properties.layerName === 'water_name' || properties.layerName === 'waterway') {
|
|
124
|
+
return 400;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return 100;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Renders label text for decoded vector-tile features, with optional geometry
|
|
132
|
+
* passthrough for debugging.
|
|
133
|
+
*/
|
|
134
|
+
export class MVTLabelLayer extends CompositeLayer<MVTLabelLayerProps> {
|
|
135
|
+
/** Deck.gl layer name. */
|
|
136
|
+
static layerName = 'MVTLabelLayer';
|
|
137
|
+
|
|
138
|
+
/** Default props for {@link MVTLabelLayer}. */
|
|
139
|
+
static defaultProps = {
|
|
140
|
+
...GeoJsonLayer.defaultProps,
|
|
141
|
+
billboard: true,
|
|
142
|
+
renderGeometry: false,
|
|
143
|
+
labelSizeUnits: 'pixels',
|
|
144
|
+
labelBackground: {type: 'color', value: null, optional: true},
|
|
145
|
+
fontFamily: 'Monaco, monospace'
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Current label-row state. */
|
|
149
|
+
state: MVTLabelLayerState = undefined!;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extracts the visible label text for a decoded feature.
|
|
153
|
+
*/
|
|
154
|
+
getLabel(feature: FeatureLike): string | undefined {
|
|
155
|
+
const {styleLayer, zoom = 0} = this.props;
|
|
156
|
+
const textField = evaluateStyleValue(styleLayer?.layout?.['text-field'], zoom);
|
|
157
|
+
const label = resolveTokenString(textField, feature.properties)?.trim();
|
|
158
|
+
return label || undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the font size for a decoded feature label.
|
|
163
|
+
*/
|
|
164
|
+
getLabelSize(_feature: FeatureLike): number {
|
|
165
|
+
const {styleLayer, zoom = 0} = this.props;
|
|
166
|
+
return Number(evaluateStyleValue(styleLayer?.layout?.['text-size'], zoom) || 14);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Returns the text color for a decoded feature label.
|
|
171
|
+
*/
|
|
172
|
+
getLabelColor(_feature: FeatureLike): number[] {
|
|
173
|
+
return this.props.textColor || [255, 255, 255];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extracts candidate label anchor positions from a feature geometry.
|
|
178
|
+
*/
|
|
179
|
+
getLabelAnchors(feature: FeatureLike): number[][] {
|
|
180
|
+
const {type, coordinates} = feature.geometry;
|
|
181
|
+
switch (type) {
|
|
182
|
+
case 'Point':
|
|
183
|
+
return [coordinates];
|
|
184
|
+
case 'MultiPoint':
|
|
185
|
+
return coordinates;
|
|
186
|
+
case 'LineString': {
|
|
187
|
+
const midpoint = getLineMidpoint(coordinates);
|
|
188
|
+
return midpoint ? [midpoint] : [];
|
|
189
|
+
}
|
|
190
|
+
case 'MultiLineString': {
|
|
191
|
+
const midpoint = getLineMidpoint(coordinates[0]);
|
|
192
|
+
return midpoint ? [midpoint] : [];
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Recomputes label anchor rows when the source tile data changes.
|
|
201
|
+
*/
|
|
202
|
+
updateState({changeFlags}: UpdateParameters<this>): void {
|
|
203
|
+
const {data} = this.props;
|
|
204
|
+
if (changeFlags.dataChanged && data) {
|
|
205
|
+
const features = Array.isArray(data) ? data : data.features || [];
|
|
206
|
+
const labelData = features.flatMap((feature, index) => {
|
|
207
|
+
const labelAnchors = this.getLabelAnchors(feature);
|
|
208
|
+
return labelAnchors.map((position) => this.getSubLayerRow({position}, feature, index));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this.setState({labelData});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Renders the optional debug geometry and the text labels.
|
|
217
|
+
*/
|
|
218
|
+
renderLayers(): any {
|
|
219
|
+
const {config, labelSizeUnits, labelBackground, billboard, renderGeometry} = this.props;
|
|
220
|
+
const layers: any[] = [];
|
|
221
|
+
|
|
222
|
+
if (renderGeometry) {
|
|
223
|
+
layers.push(
|
|
224
|
+
new GeoJsonLayer({
|
|
225
|
+
...this.props,
|
|
226
|
+
...this.getSubLayerProps({id: 'geojson'}),
|
|
227
|
+
data: this.props.data
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config.labels) {
|
|
233
|
+
const hasBackground = Array.isArray(labelBackground) && labelBackground.length >= 3;
|
|
234
|
+
layers.push(
|
|
235
|
+
new TextLayer({
|
|
236
|
+
...this.getSubLayerProps({id: 'text'}),
|
|
237
|
+
data: this.state.labelData,
|
|
238
|
+
extensions: [...(this.props.extensions || []), new CollisionFilterExtension()],
|
|
239
|
+
parameters: {
|
|
240
|
+
depthTest: false
|
|
241
|
+
},
|
|
242
|
+
billboard,
|
|
243
|
+
characterSet: 'auto',
|
|
244
|
+
collisionEnabled: true,
|
|
245
|
+
collisionGroup: 'basemap-labels',
|
|
246
|
+
getCollisionPriority: this.getSubLayerAccessor((feature: FeatureLike) =>
|
|
247
|
+
getCollisionPriority(feature)
|
|
248
|
+
) as any,
|
|
249
|
+
fontFamily: this.props.fontFamily,
|
|
250
|
+
sizeUnits: labelSizeUnits,
|
|
251
|
+
background: hasBackground,
|
|
252
|
+
getBackgroundColor: (hasBackground ? labelBackground : [0, 0, 0, 0]) as any,
|
|
253
|
+
getPosition: (d: LabelRow) => d.position,
|
|
254
|
+
getText: this.getSubLayerAccessor((feature: FeatureLike) =>
|
|
255
|
+
this.getLabel(feature)
|
|
256
|
+
) as any,
|
|
257
|
+
getSize: this.getSubLayerAccessor((feature: FeatureLike) =>
|
|
258
|
+
this.getLabelSize(feature)
|
|
259
|
+
) as any,
|
|
260
|
+
getColor: this.getSubLayerAccessor((feature: FeatureLike) =>
|
|
261
|
+
this.getLabelColor(feature)
|
|
262
|
+
) as any
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return layers;
|
|
268
|
+
}
|
|
269
|
+
}
|