Skip to content

Commit 2b7cb74

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Add atlas reuse across tiles
GitOrigin-RevId: 8f6bb215724d786093a7ccc4207663e1a3511a8e
1 parent 6292b1b commit 2b7cb74

16 files changed

Lines changed: 1228 additions & 69 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {ImageVariant} from '../style-spec/expression/types/image_variant';
2+
import {register} from '../util/web_worker_transfer';
3+
4+
import type {StyleImage, StyleImageMap} from '../style/style_image';
5+
import type {StringifiedImageVariant} from '../style-spec/expression/types/image_variant';
6+
import type {LUT} from '../util/lut';
7+
8+
type ImageDescriptor = {
9+
id: string;
10+
version: number;
11+
sx: number;
12+
sy: number;
13+
};
14+
15+
function compareImageDescriptors(a: ImageDescriptor, b: ImageDescriptor): number {
16+
if (a.id < b.id) return -1;
17+
if (a.id > b.id) return 1;
18+
if (a.version < b.version) return -1;
19+
if (a.version > b.version) return 1;
20+
if (a.sx < b.sx) return -1;
21+
if (a.sx > b.sx) return 1;
22+
if (a.sy < b.sy) return -1;
23+
if (a.sy > b.sy) return 1;
24+
return 0;
25+
}
26+
27+
function hashCode(str: string): number {
28+
let hash = 0;
29+
for (let i = 0; i < str.length; i++) {
30+
const char = str.charCodeAt(i);
31+
hash = ((hash << 5) - hash) + char;
32+
hash = hash & hash; // Convert to 32bit integer
33+
}
34+
return hash;
35+
}
36+
37+
function combineHash(hash: number, value: number): number {
38+
return ((hash << 5) - hash) + value;
39+
}
40+
41+
function addDescriptors(images: Map<StringifiedImageVariant, StyleImage>, imageVersions: ImageVersionsMap, descriptors: ImageDescriptor[], variantCache?: Map<StringifiedImageVariant, ImageVariant>) {
42+
for (const [id] of images.entries()) {
43+
// Use cached variant if available to avoid re-parsing
44+
const imageVariant = (variantCache && variantCache.get(id)) || ImageVariant.parse(id);
45+
const imageId = imageVariant.id.toString();
46+
const version = imageVersions.get(imageId) || 0;
47+
48+
descriptors.push({
49+
id: imageId,
50+
version,
51+
sx: imageVariant.sx,
52+
sy: imageVariant.sy
53+
});
54+
}
55+
}
56+
57+
export type ImageVersionsMap = Map<string, number>;
58+
59+
/**
60+
* Describes the content of an image atlas based on the requested images.
61+
* Used for caching and reusing atlases with the same content.
62+
*/
63+
export class AtlasContentDescriptor {
64+
hash: number;
65+
requiresMipMaps: boolean;
66+
67+
private iconDescriptors: ImageDescriptor[];
68+
private patternDescriptors: ImageDescriptor[];
69+
70+
constructor(
71+
icons: StyleImageMap<StringifiedImageVariant>,
72+
patterns: StyleImageMap<StringifiedImageVariant>,
73+
imageVersions: ImageVersionsMap,
74+
lut: LUT | null,
75+
variantCache?: Map<StringifiedImageVariant, ImageVariant>
76+
) {
77+
this.iconDescriptors = [];
78+
this.patternDescriptors = [];
79+
// Mipmaps are required when patterns are present
80+
this.requiresMipMaps = patterns.size > 0;
81+
82+
// Process icon images
83+
addDescriptors(icons, imageVersions, this.iconDescriptors, variantCache);
84+
85+
// Process pattern images
86+
addDescriptors(patterns, imageVersions, this.patternDescriptors, variantCache);
87+
88+
// Sort all descriptors to ensure stable hash
89+
this.iconDescriptors.sort(compareImageDescriptors);
90+
this.patternDescriptors.sort(compareImageDescriptors);
91+
92+
// Calculate combined hash
93+
let seed = 0;
94+
const lutData = lut ? lut.data : '';
95+
if (lutData) {
96+
seed = combineHash(seed, hashCode(lutData));
97+
}
98+
seed = combineHash(seed, 1); // separator
99+
100+
for (const descriptor of this.iconDescriptors) {
101+
seed = combineHash(seed, hashCode(descriptor.id));
102+
seed = combineHash(seed, descriptor.version);
103+
seed = combineHash(seed, descriptor.sx);
104+
seed = combineHash(seed, descriptor.sy);
105+
}
106+
seed = combineHash(seed, 1);
107+
for (const descriptor of this.patternDescriptors) {
108+
seed = combineHash(seed, hashCode(descriptor.id));
109+
seed = combineHash(seed, descriptor.version);
110+
seed = combineHash(seed, descriptor.sx);
111+
seed = combineHash(seed, descriptor.sy);
112+
}
113+
this.hash = seed;
114+
}
115+
116+
/**
117+
* Checks if this descriptor's content is a subset of another descriptor's content.
118+
* Returns true if all images in this descriptor are also present in the other descriptor.
119+
*/
120+
subsetOf(other: AtlasContentDescriptor): boolean {
121+
return (
122+
this.isSubsetArray(this.iconDescriptors, other.iconDescriptors, compareImageDescriptors) &&
123+
this.isSubsetArray(this.patternDescriptors, other.patternDescriptors, compareImageDescriptors)
124+
);
125+
}
126+
127+
/**
128+
* Checks if one sorted array is a subset of another sorted array.
129+
* Both arrays must be sorted using the same comparator.
130+
*/
131+
private isSubsetArray<T>(subset: T[], superset: T[], compareFn: (a: T, b: T) => number): boolean {
132+
let i = 0;
133+
let j = 0;
134+
if (subset.length > superset.length) return false;
135+
while (i < subset.length && j < superset.length) {
136+
const cmp = compareFn(subset[i], superset[j]);
137+
if (cmp === 0) {
138+
// Found a match, advance both
139+
i++;
140+
j++;
141+
} else if (cmp < 0) {
142+
// Element in subset is not in superset
143+
return false;
144+
} else {
145+
// Element in superset is larger, advance superset
146+
j++;
147+
}
148+
}
149+
// Subset is valid if we've matched all elements
150+
return i === subset.length;
151+
}
152+
153+
}
154+
155+
register(AtlasContentDescriptor, "AtlasContentDescriptor");

0 commit comments

Comments
 (0)