@tangram.gl/basemap-layers 0.0.0-stage → 0.0.1
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/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- package/src/zoom-paint-extension.ts +207 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Device, SamplerProps, Texture, TextureProps} from '@luma.gl/core';
|
|
6
|
+
|
|
7
|
+
/** The sampler deck.gl gives the textures it creates from `image` props. */
|
|
8
|
+
const DEFAULT_SAMPLER: SamplerProps = {
|
|
9
|
+
minFilter: 'linear',
|
|
10
|
+
mipmapFilter: 'linear',
|
|
11
|
+
magFilter: 'linear',
|
|
12
|
+
addressModeU: 'clamp-to-edge',
|
|
13
|
+
addressModeV: 'clamp-to-edge'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type SharedTexture = {texture: Texture; users: number};
|
|
17
|
+
|
|
18
|
+
/** Options of a `SharedTextureSet`. Sets with different options share no textures. */
|
|
19
|
+
export type SharedTextureOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Most mip levels of a texture, the full image included: 1 for none. Default: the full chain.
|
|
22
|
+
* Fewer levels keep a minified texture sharper, at the cost of aliasing beyond the last level.
|
|
23
|
+
*/
|
|
24
|
+
maxMipLevels?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shared textures of each device, by the options they were created with (see `getOptionsKey`)
|
|
29
|
+
* and the image they were created from.
|
|
30
|
+
*/
|
|
31
|
+
const sharedTextures = new WeakMap<Device, Map<string, Map<object, SharedTexture>>>();
|
|
32
|
+
|
|
33
|
+
function getOptionsKey({maxMipLevels}: SharedTextureOptions): string {
|
|
34
|
+
return maxMipLevels === undefined ? 'mip:all' : `mip:${maxMipLevels}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a texture from an image as deck.gl does for an `image` prop: an `ImageBitmap` or canvas,
|
|
39
|
+
* or a plain object with `data`, `width` and `height`, with mipmaps (at most `maxMipLevels`).
|
|
40
|
+
*/
|
|
41
|
+
function createTexture(
|
|
42
|
+
device: Device,
|
|
43
|
+
image: object,
|
|
44
|
+
{maxMipLevels}: SharedTextureOptions
|
|
45
|
+
): Texture {
|
|
46
|
+
const source = (
|
|
47
|
+
image.constructor && image.constructor.name !== 'Object' ? {data: image} : image
|
|
48
|
+
) as {data: {width?: number; height?: number}; width?: number; height?: number};
|
|
49
|
+
// A plain object gives its size; raw pixel data has none of its own.
|
|
50
|
+
const width = source.width ?? source.data.width ?? 1;
|
|
51
|
+
const height = source.height ?? source.data.height ?? 1;
|
|
52
|
+
const texture = device.createTexture({
|
|
53
|
+
...source,
|
|
54
|
+
sampler: DEFAULT_SAMPLER,
|
|
55
|
+
mipLevels: Math.max(
|
|
56
|
+
1,
|
|
57
|
+
Math.min(maxMipLevels ?? Infinity, device.getMipLevelCount(width, height))
|
|
58
|
+
)
|
|
59
|
+
} as TextureProps);
|
|
60
|
+
if (device.type === 'webgl') {
|
|
61
|
+
texture.generateMipmapsWebGL();
|
|
62
|
+
} else if (device.type === 'webgpu') {
|
|
63
|
+
(device as any).generateMipmapsWebGPU(texture);
|
|
64
|
+
}
|
|
65
|
+
return texture;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The textures one layer uses, shared with every other layer that uses the same image on the
|
|
70
|
+
* same device. deck.gl creates a texture, and uploads the image, for each layer that is handed an
|
|
71
|
+
* image: every label sublayer of every tile would upload the same font atlas and sprite sheet. A
|
|
72
|
+
* shared texture is created on first use and destroyed when its last user releases it.
|
|
73
|
+
*/
|
|
74
|
+
export class SharedTextureSet {
|
|
75
|
+
private images = new Set<object>();
|
|
76
|
+
/** The options, as given at construction: they are part of the cache key. */
|
|
77
|
+
readonly options: Readonly<SharedTextureOptions>;
|
|
78
|
+
private optionsKey: string;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private device: Device,
|
|
82
|
+
{maxMipLevels}: SharedTextureOptions = {}
|
|
83
|
+
) {
|
|
84
|
+
this.options = {maxMipLevels};
|
|
85
|
+
this.optionsKey = getOptionsKey(this.options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The textures shared by the sets with this set's options on its device. */
|
|
89
|
+
private getTextures(): Map<object, SharedTexture> {
|
|
90
|
+
let byOptions = sharedTextures.get(this.device);
|
|
91
|
+
if (!byOptions) {
|
|
92
|
+
byOptions = new Map();
|
|
93
|
+
sharedTextures.set(this.device, byOptions);
|
|
94
|
+
}
|
|
95
|
+
let textures = byOptions.get(this.optionsKey);
|
|
96
|
+
if (!textures) {
|
|
97
|
+
textures = new Map();
|
|
98
|
+
byOptions.set(this.optionsKey, textures);
|
|
99
|
+
}
|
|
100
|
+
return textures;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Returns the shared texture of `image`, which this set uses until `retainOnly` drops it. */
|
|
104
|
+
get(image: object): Texture {
|
|
105
|
+
const textures = this.getTextures();
|
|
106
|
+
let entry = textures.get(image);
|
|
107
|
+
if (!entry) {
|
|
108
|
+
entry = {texture: createTexture(this.device, image, this.options), users: 0};
|
|
109
|
+
textures.set(image, entry);
|
|
110
|
+
}
|
|
111
|
+
if (!this.images.has(image)) {
|
|
112
|
+
this.images.add(image);
|
|
113
|
+
entry.users++;
|
|
114
|
+
}
|
|
115
|
+
return entry.texture;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Releases the textures of every image this set uses that is not in `images`. */
|
|
119
|
+
retainOnly(images: Iterable<object>): void {
|
|
120
|
+
const kept = new Set(images);
|
|
121
|
+
for (const image of this.images) {
|
|
122
|
+
if (!kept.has(image)) {
|
|
123
|
+
this.release(image);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Releases every texture this set uses. */
|
|
129
|
+
releaseAll(): void {
|
|
130
|
+
for (const image of this.images) {
|
|
131
|
+
this.release(image);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private release(image: object): void {
|
|
136
|
+
this.images.delete(image);
|
|
137
|
+
const textures = this.getTextures();
|
|
138
|
+
const entry = textures.get(image);
|
|
139
|
+
if (entry && --entry.users <= 0) {
|
|
140
|
+
entry.texture.destroy();
|
|
141
|
+
textures.delete(image);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The number of shared textures alive on a device, for tests. */
|
|
147
|
+
export function getSharedTextureCount(device: Device): number {
|
|
148
|
+
let count = 0;
|
|
149
|
+
for (const textures of sharedTextures.get(device)?.values() || []) {
|
|
150
|
+
count += textures.size;
|
|
151
|
+
}
|
|
152
|
+
return count;
|
|
153
|
+
}
|
package/src/sort-key.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
// deck.gl-community
|
|
6
|
+
// SPDX-License-Identifier: MIT
|
|
7
|
+
// Copyright (c) vis.gl contributors
|
|
8
|
+
|
|
9
|
+
import {getStyleAccessor} from './style-accessor';
|
|
10
|
+
import type {ResolvedBasemapStyleLayer as BasemapStyleLayer} from './style-resolver';
|
|
11
|
+
|
|
12
|
+
/** The latest sorted features per tile content and style layer. */
|
|
13
|
+
const sortedFeatureCache = new WeakMap<
|
|
14
|
+
any[],
|
|
15
|
+
WeakMap<BasemapStyleLayer, {key: unknown[]; features: any[]}>
|
|
16
|
+
>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A tile's features of a style layer in the order of their sort key (`circle-sort-key`,
|
|
20
|
+
* `line-sort-key`), ascending, as MapLibre GL JS orders a bucket: features with a higher key draw
|
|
21
|
+
* over those with a lower one, and features with the same key keep their tile order. Without a
|
|
22
|
+
* per-feature key, the features are returned as they are. The sorted array is reused while the
|
|
23
|
+
* features, the key and (for a key that depends on zoom) the style zoom are unchanged, so deck.gl
|
|
24
|
+
* does not re-tessellate the features at every zoom step.
|
|
25
|
+
*/
|
|
26
|
+
export function sortFeaturesByKey(
|
|
27
|
+
features: any[],
|
|
28
|
+
styleLayer: BasemapStyleLayer,
|
|
29
|
+
propertyName: 'circle-sort-key' | 'line-sort-key',
|
|
30
|
+
zoom: number
|
|
31
|
+
): any[] {
|
|
32
|
+
const sortKey = styleLayer.layout?.[propertyName];
|
|
33
|
+
if (sortKey === undefined) {
|
|
34
|
+
return features;
|
|
35
|
+
}
|
|
36
|
+
const accessor = getStyleAccessor(styleLayer, [propertyName], zoom, ([key]) => Number(key) || 0);
|
|
37
|
+
const {value} = accessor;
|
|
38
|
+
const key = [
|
|
39
|
+
propertyName,
|
|
40
|
+
sortKey,
|
|
41
|
+
features.length,
|
|
42
|
+
typeof value === 'function' ? accessor.updateTrigger : value
|
|
43
|
+
];
|
|
44
|
+
let byStyleLayer = sortedFeatureCache.get(features);
|
|
45
|
+
if (!byStyleLayer) {
|
|
46
|
+
byStyleLayer = new WeakMap();
|
|
47
|
+
sortedFeatureCache.set(features, byStyleLayer);
|
|
48
|
+
}
|
|
49
|
+
const cached = byStyleLayer.get(styleLayer);
|
|
50
|
+
if (cached && cached.key.every((entry, index) => entry === key[index])) {
|
|
51
|
+
return cached.features;
|
|
52
|
+
}
|
|
53
|
+
// `Array.prototype.sort` is stable.
|
|
54
|
+
const sorted =
|
|
55
|
+
typeof value === 'function'
|
|
56
|
+
? features
|
|
57
|
+
.map(feature => ({feature, key: value(feature)}))
|
|
58
|
+
.sort((a, b) => a.key - b.key)
|
|
59
|
+
.map(entry => entry.feature)
|
|
60
|
+
: features;
|
|
61
|
+
byStyleLayer.set(styleLayer, {key, features: sorted});
|
|
62
|
+
return sorted;
|
|
63
|
+
}
|
package/src/sprite.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {log} from '@deck.gl/core';
|
|
6
|
+
import type {BasemapLoadOptions} from './style-resolver';
|
|
7
|
+
|
|
8
|
+
/** One image in a style sprite, as listed in the sprite's JSON index. */
|
|
9
|
+
export type SpriteImage = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
/** Device pixels per CSS pixel of the image. Default 1. */
|
|
15
|
+
pixelRatio?: number;
|
|
16
|
+
/** `true` for signed-distance-field images, which are tinted with `icon-color`. */
|
|
17
|
+
sdf?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A deck.gl `IconLayer` mapping entry for one sprite image. */
|
|
21
|
+
export type SpriteIconMappingEntry = {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
/** `true` for SDF images: deck.gl tints masked icons with `getColor`. */
|
|
27
|
+
mask: boolean;
|
|
28
|
+
/** Device pixels per CSS pixel; `icon-size` scales `height / pixelRatio`. */
|
|
29
|
+
pixelRatio: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** A loaded sprite: the atlas image and its deck.gl icon mapping. */
|
|
33
|
+
export type SpriteAtlas = {
|
|
34
|
+
/**
|
|
35
|
+
* Sprite id. `'default'` for a style whose `sprite` is a single URL; otherwise the id from the
|
|
36
|
+
* style's sprite array, which prefixes its image names as `id:name`.
|
|
37
|
+
*/
|
|
38
|
+
id: string;
|
|
39
|
+
/**
|
|
40
|
+
* The atlas image, decoded once and shared by every icon layer, or its URL where
|
|
41
|
+
* `createImageBitmap` is unavailable (each `IconLayer` then loads the URL itself).
|
|
42
|
+
*/
|
|
43
|
+
image: ImageBitmap | string;
|
|
44
|
+
/** Icon mapping keyed by image name, without the `id:` prefix. */
|
|
45
|
+
mapping: Record<string, SpriteIconMappingEntry>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A style's `sprite` value: a URL, or the array form with one `{id, url}` per sprite. */
|
|
49
|
+
export type StyleSprite = string | Array<{id: string; url: string}>;
|
|
50
|
+
|
|
51
|
+
/** A sprite source after relative URLs are resolved. */
|
|
52
|
+
export type SpriteSource = {id: string; url: string};
|
|
53
|
+
|
|
54
|
+
/** Options for {@link loadSpriteAtlases}. */
|
|
55
|
+
export type SpriteLoadOptions = {
|
|
56
|
+
/** Base URL that relative sprite URLs resolve against, normally the style URL. */
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
/** Device pixel ratio. At 2 or more, the `@2x` sprite is requested first. Default 1. */
|
|
59
|
+
pixelRatio?: number;
|
|
60
|
+
} & Pick<NonNullable<BasemapLoadOptions>, 'fetch' | 'fetchOptions'>;
|
|
61
|
+
|
|
62
|
+
/** Resolves a style's `sprite` value into sprite sources with absolute URLs. */
|
|
63
|
+
export function getSpriteSources(sprite: unknown, baseUrl?: string): SpriteSource[] {
|
|
64
|
+
const resolve = (url: string) => {
|
|
65
|
+
try {
|
|
66
|
+
return new URL(url, baseUrl).toString();
|
|
67
|
+
} catch {
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
if (typeof sprite === 'string' && sprite) {
|
|
72
|
+
return [{id: 'default', url: resolve(sprite)}];
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(sprite)) {
|
|
75
|
+
return sprite
|
|
76
|
+
.filter(entry => entry && typeof entry.id === 'string' && typeof entry.url === 'string')
|
|
77
|
+
.map(entry => ({id: entry.id, url: resolve(entry.url)}));
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Converts a sprite JSON index into a deck.gl `IconLayer` mapping. */
|
|
83
|
+
export function getSpriteIconMapping(
|
|
84
|
+
index: Record<string, SpriteImage>
|
|
85
|
+
): Record<string, SpriteIconMappingEntry> {
|
|
86
|
+
const mapping: Record<string, SpriteIconMappingEntry> = {};
|
|
87
|
+
for (const [name, image] of Object.entries(index || {})) {
|
|
88
|
+
mapping[name] = {
|
|
89
|
+
x: image.x,
|
|
90
|
+
y: image.y,
|
|
91
|
+
width: image.width,
|
|
92
|
+
height: image.height,
|
|
93
|
+
mask: Boolean(image.sdf),
|
|
94
|
+
pixelRatio: image.pixelRatio || 1
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return mapping;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Decodes the atlas PNG once, so the per-tile icon layers share one image instead of each
|
|
102
|
+
* requesting it. Without `createImageBitmap` (e.g. in Node), returns the URL.
|
|
103
|
+
*/
|
|
104
|
+
async function loadSpriteImage(
|
|
105
|
+
url: string,
|
|
106
|
+
fetchFn: typeof fetch,
|
|
107
|
+
fetchOptions?: RequestInit
|
|
108
|
+
): Promise<ImageBitmap | string> {
|
|
109
|
+
if (typeof createImageBitmap !== 'function') {
|
|
110
|
+
return url;
|
|
111
|
+
}
|
|
112
|
+
const response = await fetchFn(url, fetchOptions);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`Failed to load sprite image: ${url} (${response.status})`);
|
|
115
|
+
}
|
|
116
|
+
return await createImageBitmap(await response.blob());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Appends a suffix and extension to a sprite URL's path, keeping its query string and hash. */
|
|
120
|
+
function getSpriteFileUrl(url: string, suffix: string, extension: string): string {
|
|
121
|
+
const pathEnd = url.search(/[?#]/);
|
|
122
|
+
return pathEnd < 0
|
|
123
|
+
? `${url}${suffix}${extension}`
|
|
124
|
+
: `${url.slice(0, pathEnd)}${suffix}${extension}${url.slice(pathEnd)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Loads one sprite: the `@2x` variant first at any pixel ratio above 1, as MapLibre does, falling
|
|
129
|
+
* back to `@1x`.
|
|
130
|
+
*/
|
|
131
|
+
async function loadSpriteAtlas(
|
|
132
|
+
source: SpriteSource,
|
|
133
|
+
options: SpriteLoadOptions
|
|
134
|
+
): Promise<SpriteAtlas> {
|
|
135
|
+
const fetchFn = options.fetch || fetch;
|
|
136
|
+
const suffixes = (options.pixelRatio ?? 1) > 1 ? ['@2x', ''] : [''];
|
|
137
|
+
let lastError: unknown;
|
|
138
|
+
for (const suffix of suffixes) {
|
|
139
|
+
const jsonUrl = getSpriteFileUrl(source.url, suffix, '.json');
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetchFn(jsonUrl, options.fetchOptions);
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`Failed to load sprite: ${jsonUrl} (${response.status})`);
|
|
144
|
+
}
|
|
145
|
+
const index = (await response.json()) as Record<string, SpriteImage>;
|
|
146
|
+
return {
|
|
147
|
+
id: source.id,
|
|
148
|
+
image: await loadSpriteImage(
|
|
149
|
+
getSpriteFileUrl(source.url, suffix, '.png'),
|
|
150
|
+
fetchFn,
|
|
151
|
+
options.fetchOptions
|
|
152
|
+
),
|
|
153
|
+
mapping: getSpriteIconMapping(index)
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
lastError = error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw lastError;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Loads every sprite a style references. A sprite that fails to load is skipped with a warning,
|
|
164
|
+
* so its icons are not drawn and the rest of the style still renders.
|
|
165
|
+
*/
|
|
166
|
+
export async function loadSpriteAtlases(
|
|
167
|
+
sprite: unknown,
|
|
168
|
+
options: SpriteLoadOptions = {}
|
|
169
|
+
): Promise<SpriteAtlas[]> {
|
|
170
|
+
const sources = getSpriteSources(sprite, options.baseUrl);
|
|
171
|
+
const atlases = await Promise.all(
|
|
172
|
+
sources.map(source =>
|
|
173
|
+
loadSpriteAtlas(source, options).catch(error => {
|
|
174
|
+
log.warn(
|
|
175
|
+
`[BasemapLayer] Sprite ${source.url} failed to load; its icons are skipped`,
|
|
176
|
+
error
|
|
177
|
+
)();
|
|
178
|
+
return null;
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
return atlases.filter((atlas): atlas is SpriteAtlas => Boolean(atlas));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const imageNamesCache = new WeakMap<readonly SpriteAtlas[], string[]>();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The image names `icon-image` can reference: unprefixed for the `'default'` sprite and `id:name`
|
|
189
|
+
* for the others. Passed to expression evaluation so `["image", ...]` sees which images exist.
|
|
190
|
+
*/
|
|
191
|
+
export function getSpriteImageNames(atlases: readonly SpriteAtlas[] | null | undefined): string[] {
|
|
192
|
+
if (!atlases) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
let names = imageNamesCache.get(atlases);
|
|
196
|
+
if (!names) {
|
|
197
|
+
names = atlases.flatMap(atlas =>
|
|
198
|
+
Object.keys(atlas.mapping).map(name =>
|
|
199
|
+
atlas.id === 'default' ? name : `${atlas.id}:${name}`
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
imageNamesCache.set(atlases, names);
|
|
203
|
+
}
|
|
204
|
+
return names;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** An icon resolved against the loaded sprites. */
|
|
208
|
+
export type ResolvedSpriteIcon = {atlas: SpriteAtlas; name: string; entry: SpriteIconMappingEntry};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Finds the sprite and mapping entry for an `icon-image` name. Names from the `'default'` sprite
|
|
212
|
+
* are unprefixed; names from other sprites are written `id:name`, as in MapLibre.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveSpriteIcon(
|
|
215
|
+
atlases: readonly SpriteAtlas[] | undefined,
|
|
216
|
+
iconImage: string
|
|
217
|
+
): ResolvedSpriteIcon | null {
|
|
218
|
+
if (!atlases || !iconImage) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const separator = iconImage.indexOf(':');
|
|
222
|
+
if (separator > 0) {
|
|
223
|
+
const atlas = atlases.find(candidate => candidate.id === iconImage.slice(0, separator));
|
|
224
|
+
const name = iconImage.slice(separator + 1);
|
|
225
|
+
if (atlas?.mapping[name]) {
|
|
226
|
+
return {atlas, name, entry: atlas.mapping[name]};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const atlas = atlases.find(candidate => candidate.id === 'default');
|
|
230
|
+
if (atlas?.mapping[iconImage]) {
|
|
231
|
+
return {atlas, name: iconImage, entry: atlas.mapping[iconImage]};
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const warnedMissingIcons = new Set<string>();
|
|
237
|
+
|
|
238
|
+
/** Warns once per icon name that a style references an image no loaded sprite contains. */
|
|
239
|
+
export function warnMissingIcon(iconImage: string): void {
|
|
240
|
+
if (!warnedMissingIcons.has(iconImage)) {
|
|
241
|
+
warnedMissingIcons.add(iconImage);
|
|
242
|
+
log.warn(`[BasemapLayer] icon-image "${iconImage}" is not in the style's sprite; skipped`)();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {getCompiledStyleProperty, type StyleFeature} from './style-expression';
|
|
6
|
+
|
|
7
|
+
type StyleLayerLike = {
|
|
8
|
+
paint?: Record<string, unknown>;
|
|
9
|
+
layout?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Zoom step at which style values are evaluated. Paint and layout values step every
|
|
14
|
+
* `STYLE_ZOOM_STEP` zoom levels instead of following the zoom continuously.
|
|
15
|
+
*/
|
|
16
|
+
export const STYLE_ZOOM_STEP = 0.25;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Zoom at which paint and layout values are evaluated: the zoom rounded down to a
|
|
20
|
+
* `STYLE_ZOOM_STEP`.
|
|
21
|
+
*
|
|
22
|
+
* This is a temporary simplification, not MapLibre's behaviour. MapLibre evaluates zoom-dependent
|
|
23
|
+
* expressions at the integer zooms on either side of the current zoom and interpolates between
|
|
24
|
+
* them on the GPU, which remains the long-term plan here. Stepping keeps every tile on the same
|
|
25
|
+
* evaluation zoom, so neighbouring tiles agree at their seams.
|
|
26
|
+
*/
|
|
27
|
+
export function getZoomBucket(zoom: number): number {
|
|
28
|
+
return Math.floor(zoom / STYLE_ZOOM_STEP) * STYLE_ZOOM_STEP;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Zoom at which filters are evaluated: the integer zoom, as the style spec specifies. */
|
|
32
|
+
export function getFilterZoom(zoom: number): number {
|
|
33
|
+
return Math.floor(zoom);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A key that changes whenever style evaluation or layer visibility changes: at every
|
|
38
|
+
* `STYLE_ZOOM_STEP` (which includes every integer zoom, where filters change), and at every
|
|
39
|
+
* fractional `minzoom`/`maxzoom` in `zoomLimits`. Layer visibility uses the exact zoom, as in
|
|
40
|
+
* MapLibre, so crossing a fractional limit must regenerate sublayers too.
|
|
41
|
+
*/
|
|
42
|
+
export function getStyleZoomKey(zoom: number, zoomLimits: readonly (number | undefined)[]): string {
|
|
43
|
+
const crossed = zoomLimits.filter(
|
|
44
|
+
limit => limit !== undefined && !Number.isInteger(limit) && zoom >= limit
|
|
45
|
+
).length;
|
|
46
|
+
return `${getZoomBucket(zoom)}:${crossed}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A deck.gl accessor built from one or more style properties. */
|
|
50
|
+
export type StyleAccessor<T> = {
|
|
51
|
+
/** A constant when no property depends on feature data, otherwise a per-feature function. */
|
|
52
|
+
value: T | ((feature: StyleFeature) => T);
|
|
53
|
+
/** Set to the evaluation zoom (`getZoomBucket`) when a per-feature value also depends on zoom. */
|
|
54
|
+
updateTrigger?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds a deck.gl accessor from style properties, evaluated at the stepped zoom
|
|
59
|
+
* (`getZoomBucket`). Feature-independent values become constants. Data-driven values become
|
|
60
|
+
* per-feature functions, with an update trigger when they also depend on zoom.
|
|
61
|
+
*/
|
|
62
|
+
export function getStyleAccessor<T>(
|
|
63
|
+
styleLayer: StyleLayerLike,
|
|
64
|
+
propertyNames: string[],
|
|
65
|
+
zoom: number,
|
|
66
|
+
combine: (values: any[]) => T
|
|
67
|
+
): StyleAccessor<T> {
|
|
68
|
+
const properties = propertyNames.map(name => getCompiledStyleProperty(styleLayer, name));
|
|
69
|
+
const isFeatureDependent = properties.some(property => property?.isFeatureDependent);
|
|
70
|
+
const zoomBucket = getZoomBucket(zoom);
|
|
71
|
+
|
|
72
|
+
if (!isFeatureDependent) {
|
|
73
|
+
return {value: combine(properties.map(property => property?.evaluate(zoomBucket)))};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isZoomDependent = properties.some(property => property?.isZoomDependent);
|
|
77
|
+
return {
|
|
78
|
+
value: (feature: StyleFeature) =>
|
|
79
|
+
combine(properties.map(property => property?.evaluate(zoomBucket, feature))),
|
|
80
|
+
updateTrigger: isZoomDependent ? zoomBucket : undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Converts a style color (alpha 0-1 or 0-255) and opacity into a deck.gl RGBA color. */
|
|
85
|
+
export function withOpacity(
|
|
86
|
+
color: number[] | null | undefined,
|
|
87
|
+
opacity = 1
|
|
88
|
+
): [number, number, number, number] {
|
|
89
|
+
if (!color) {
|
|
90
|
+
return [0, 0, 0, 0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const alpha = color.length > 3 ? (color[3] <= 1 ? color[3] * 255 : color[3]) : 255;
|
|
94
|
+
return [color[0], color[1], color[2], Math.round(alpha * opacity)];
|
|
95
|
+
}
|