gistools/writers/to_tiles/worker.rs
1use super::{
2 BuildGuide, FormatOutput, LayerGuide, LayerHandler, MVectorFeature, ToTileMetadata,
3 VectorLayerGuide,
4};
5use crate::{
6 data_store::{MultiMap, MultiMapStore},
7 data_structures::{HasLayer, PointCluster, PointGrid, TileStore, TileStoreOptions},
8 geometry::S2CellId,
9 parsers::RGBA,
10 util::{CompressionFormat, compress_data},
11};
12use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
13use core::mem::take;
14use earclip::{earclip, tesselate};
15use libm::{floor, fmax, fmin};
16use open_vector_tile::{
17 base::{BaseVectorLayer, BaseVectorTile, s2json_to_base},
18 mapbox, write_tile,
19};
20use s2json::{
21 Face, JSONCollection, MValue, MValueCompatible, Projection, VectorFeature, VectorGeometry,
22 VectorPolygon,
23};
24
25/// A built tile that is ready to be written to the filesystem
26#[derive(Debug)]
27pub struct BuiltTile {
28 /// Face of the tile
29 pub face: Face,
30 /// zoom of the tile
31 pub zoom: u8,
32 /// x tile coordinate
33 pub x: u32,
34 /// y tile coordinate
35 pub y: u32,
36 /// compressed data
37 pub data: Vec<u8>,
38}
39
40/// Convert a vector feature to a collection of tiles and store each tile feature
41#[derive(Debug)]
42pub struct TileWorker {
43 /// Id of the worker (useful for threads)
44 pub id: usize,
45 /// total minzoom
46 pub minzoom: u8,
47 /// total maxzoom
48 pub maxzoom: u8,
49 /// Description of how to build data
50 build_guide: BuildGuide,
51 /// Store Vector Features whose key are Tile IDs.
52 pub vector_store: MultiMap<S2CellId, MVectorFeature>,
53 /// Unique store for each layer that describes itself as a cluster source
54 pub cluster_stores: BTreeMap<String, PointCluster<MValue>>, /* { [layerName: string]: PointCluster } = {}; */
55 /// Unique store for each layer that describes itself as a raster source
56 pub raster_stores: BTreeMap<String, PointGrid<RGBA>>, /* { [layerName: string]: PointGrid<RGBA> } = {}; */
57 /// Unique store for each layer that describes itself as a grid source
58 pub grid_stores: BTreeMap<String, PointGrid<f64>>, // { [layerName: string]: PointGrid } = {};
59}
60impl TileWorker {
61 /// Create a new TileWorker
62 pub fn new(id: usize, build_guide: BuildGuide) -> Self {
63 let (minzoom, maxzoom) = get_zooms(&build_guide.layer_guides);
64 Self {
65 id,
66 minzoom,
67 maxzoom,
68 build_guide,
69 vector_store: MultiMap::<S2CellId, MVectorFeature>::new(None),
70 cluster_stores: BTreeMap::new(),
71 raster_stores: BTreeMap::new(),
72 grid_stores: BTreeMap::new(),
73 }
74 }
75 /// Iterate through all the stores and sort/cluster as needed
76 pub fn sort(&mut self) {
77 for cluster in self.cluster_stores.values_mut() {
78 cluster.build_clusters(None);
79 }
80 for raster in self.raster_stores.values_mut() {
81 raster.build_clusters();
82 }
83 for grid in self.grid_stores.values_mut() {
84 grid.build_clusters();
85 }
86 }
87
88 /// Store a feature to the appropriate store
89 ///
90 /// ## Parameters
91 /// - `feature`: The feature to store
92 /// - `source_name`: The name of the source to store the feature in
93 /// - `on_layer_feature`: An optional handler to update or filter the feature
94 pub fn store_feature<M: Clone + HasLayer, P: MValueCompatible, D: MValueCompatible>(
95 &mut self,
96 mut feature: VectorFeature<M, P, D>,
97 source_name: String,
98 on_layer_feature: Option<&Vec<LayerHandler<M, P, D>>>,
99 ) {
100 // First find all build guide layers that use this source
101 let layer_guides = self
102 .build_guide
103 .layer_guides
104 .iter()
105 .filter(|lg| lg.has_source(&source_name))
106 .cloned()
107 .collect::<Vec<_>>();
108 // iterate through each layer and store the feature
109 for layer_guide in layer_guides {
110 // find the layer handler that matches layer_guide's layer_name and mutate if needed
111 if let Some(on_layer_feature) = &on_layer_feature
112 && let Some(layer_handler) =
113 on_layer_feature.iter().find(|lh| lh.layer_name == layer_guide.layer_name())
114 && let Some(new_feature) = (layer_handler.on_feature)(take(&mut feature))
115 {
116 feature = new_feature;
117 }
118 match layer_guide {
119 LayerGuide::Vector(vlg) => {
120 self.store_vector_feature(
121 feature.to_m_vector_feature(|_| {
122 Some(ToTileMetadata::new(vlg.base.layer_name.clone()))
123 }),
124 vlg,
125 );
126 }
127 // LayerGuide::Cluster(clg) => {
128 // self.store_cluster_feature(
129 // feature.to_m_vector_feature(|_| {
130 // Some(ToTileMetadata::new(clg.base.layer_name.clone()))
131 // }),
132 // clg,
133 // );
134 // }
135 _ => {
136 unimplemented!()
137 }
138 }
139 }
140 }
141
142 /// Store a vector feature across all appropriate zooms
143 ///
144 /// ## Parameters
145 /// - `feature`: The vector feature to store
146 /// - `layer`: The layer guide on how to shape for storage
147 pub fn store_vector_feature(&mut self, feature: MVectorFeature, layer: VectorLayerGuide) {
148 let BuildGuide { projection, .. } = &self.build_guide;
149 // skip features who are not using the layer guide's
150 if !layer.draw_types.contains(&(&feature).into()) {
151 return;
152 }
153 let minzoom = layer.vector_guide.minzoom.unwrap_or(0);
154 let maxzoom = layer.vector_guide.maxzoom.unwrap_or(self.maxzoom);
155 // Setup a tile_cache and dive down. Store the 4 children if data is found while storing data as we go
156 let mut tile_store = TileStore::new(
157 JSONCollection::VectorFeature(feature),
158 TileStoreOptions { projection: Some(*projection), ..layer.vector_guide },
159 );
160 let mut tile_cache = vec![S2CellId::from_face(0)];
161 if *projection == Projection::S2 {
162 tile_cache.extend([
163 S2CellId::from_face(1),
164 S2CellId::from_face(2),
165 S2CellId::from_face(3),
166 S2CellId::from_face(4),
167 S2CellId::from_face(5),
168 ]);
169 }
170 while let Some(id) = tile_cache.pop() {
171 let zoom = id.level();
172 if zoom > maxzoom {
173 continue;
174 }
175 let tile = tile_store.get_tile(id);
176 if zoom < minzoom {
177 // if we haven't reached the data yet, we store children
178 tile_cache.extend(id.children(None));
179 } else if let Some(tile) = tile
180 && !tile.is_empty()
181 {
182 // store feature with the associated layername
183 for layer in tile.layers.values() {
184 for feature in &layer.features {
185 self.vector_store.set(id, feature.clone());
186 }
187 }
188 // store 4 children tiles to ask for
189 tile_cache.extend(id.children(None));
190 }
191 }
192 }
193
194 /// Get vector/cluster features for a tile
195 ///
196 /// ## Parameters
197 /// - `id`: tile id
198 ///
199 /// ## Returns
200 /// The shaped vector tile for storage if applicable
201 fn get_vector_tile(&mut self, id: S2CellId) -> Option<BaseVectorTile> {
202 let BuildGuide { layer_guides, format, build_indices, .. } = &self.build_guide;
203 if *format == FormatOutput::Raster {
204 return None;
205 }
206 let mut res = BaseVectorTile::default();
207 let zoom = id.level();
208 // store vector features
209 if let Some(vector_features) = self.vector_store.get(id).cloned() {
210 for mut feature in vector_features {
211 let layer_name = feature.metadata.as_ref().unwrap().layer_name.clone();
212 let layer = layer_guides
213 .iter()
214 .find(|lg| lg.layer_name() == layer_name)
215 .unwrap()
216 .to_vector()
217 .unwrap();
218 if *build_indices {
219 // pre-earclip, since we are still working with floats, the extent of 1 is fine! :D
220 earclip_polygons(&mut feature, zoom);
221 }
222 let layer = res.layers.entry(layer_name.clone()).or_insert(BaseVectorLayer::new(
223 layer_name,
224 layer.extent,
225 vec![],
226 layer.shape,
227 layer.m_shape,
228 ));
229 layer.add_feature(s2json_to_base(&feature, layer.extent));
230 }
231 }
232 // // store all cluster features
233 // for (const [layerName, cluster] of Object.entries(this.cluster_stores)) {
234 // const layerClusterFeatures = await cluster.getTile(id);
235 // if (layerClusterFeatures === undefined) continue;
236 // for (const layer of Object.values(layerClusterFeatures.layers)) {
237 // for (const feature of layer.features) tile.addFeature(feature, layerName);
238 // }
239 // }
240
241 if res.layers.is_empty() { None } else { Some(res) }
242 }
243
244 /// Iterate through all the stores and sort/cluster as needed and build tiles
245 pub fn build_tiles(&mut self) -> TileWorkerTileBuilder<'_> {
246 let mut tile_stack = vec![S2CellId::from_face(0)];
247 if self.build_guide.projection == Projection::S2 {
248 tile_stack.extend([
249 S2CellId::from_face(1),
250 S2CellId::from_face(2),
251 S2CellId::from_face(3),
252 S2CellId::from_face(4),
253 S2CellId::from_face(5),
254 ]);
255 }
256 TileWorkerTileBuilder { worker: self, tile_stack }
257 }
258}
259/// Iterate through the stores and build tiles, compressing as we go if required
260#[derive(Debug)]
261pub struct TileWorkerTileBuilder<'a> {
262 worker: &'a mut TileWorker,
263 tile_stack: Vec<S2CellId>,
264}
265impl Iterator for TileWorkerTileBuilder<'_> {
266 type Item = BuiltTile;
267 fn next(&mut self) -> Option<Self::Item> {
268 let build_guide = &self.worker.build_guide;
269 let format = build_guide.format.clone();
270 let encoding: CompressionFormat = (&build_guide.encoding).into();
271 while let Some(id) = self.tile_stack.pop() {
272 // if the current id is less than our target zoom, we add children to the stack and continue
273 let (face, zoom, x, y) = id.to_face_ij();
274 if zoom > self.worker.maxzoom {
275 continue;
276 }
277 if zoom < self.worker.minzoom {
278 self.tile_stack.extend(id.children(None));
279 continue;
280 }
281 let mut vector_tile = self.worker.get_vector_tile(id);
282 // otherwise, we build the tile
283 if format != FormatOutput::Raster {
284 let mut data = if format == FormatOutput::OpenS2 {
285 write_tile(vector_tile.as_mut(), None, None)
286 } else if let Some(mut vector_tile) = vector_tile {
287 mapbox::vector_tile::write_tile(
288 &mut vector_tile,
289 format == FormatOutput::Mapbox,
290 )
291 } else {
292 vec![]
293 };
294 if data.is_empty() {
295 continue;
296 } else {
297 data = compress_data(data, encoding).unwrap();
298 self.tile_stack.extend(id.children(None));
299 return Some(BuiltTile { face: face.into(), zoom, x, y, data });
300 }
301 }
302 }
303 None
304 }
305}
306
307// /**
308// * Iterate through the stores and build tiles, compressing as we go if required
309// * @yields - a built tile
310// */
311// async *buildTiles(): AsyncGenerator<BuiltTile> {
312// const { format, layer_guides, projection, encoding } = this;
313// const minzoom = getMinzoom(layer_guides);
314
315// // three directions we can build data
316// const tile_cache = [idFromFace(0)];
317// if (projection === 'S2')
318// tile_cache.push(idFromFace(1), idFromFace(2), idFromFace(3), idFromFace(4), idFromFace(5));
319// while (tile_cache.length > 0) {
320// const id = tile_cache.pop()!;
321// const tile = new Tile(id);
322// const { face, zoom, i: x, j: y } = tile;
323
324// const vectorTile = await this.#get_vector_tile(id, tile);
325// const rasterData = await this.#getRasterTile(id);
326// const gridData = await this.#getGridTile(id);
327// if (format === 'raster') {
328// // RASTER CASE
329// if (rasterData !== undefined) {
330// const data = new Uint8Array(rasterData[0].image);
331// yield { face, zoom, x, y, data };
332// // store 4 children tiles to ask for children features
333// tile_cache.push(...idChildrenIJ(face, zoom, x, y));
334// } else {
335// // if we haven't reached the data yet, we store children
336// if (minzoom > tile.zoom) tile_cache.push(...idChildrenIJ(face, zoom, x, y));
337// }
338// } else {
339// // VECTOR CASE
340// if (vectorTile === undefined && rasterData === undefined && gridData === undefined) {
341// // if we haven't reached the data yet, we store children
342// if (minzoom > tile.zoom) tile_cache.push(...idChildrenIJ(face, zoom, x, y));
343// } else {
344// // write to a buffer using the open-vector-tile spec
345// let data =
346// format === 'open-s2'
347// ? writeOVTile(vectorTile, rasterData, gridData)
348// : writeMVTile(vectorTile!, format === 'mapbox');
349// // gzip if necessary
350// if (encoding === 'gz') data = await compressStream(data, 'gzip');
351// // yield the buffer
352// yield { face, zoom, x, y, data };
353// // store 4 children tiles to ask for children features
354// tile_cache.push(...idChildrenIJ(face, zoom, x, y));
355// }
356// }
357// }
358// }
359
360// /**
361// * Get raster data for a tile
362// *
363// * ## Parameters
364// * - `id`: the tile id
365// *
366// * ## Returns
367// * A collection of GridInputs
368// */
369// async #getRasterTile(id: S2CellId): Promise<ImageDataInput[] | undefined> {
370// const res: ImageDataInput[] = [];
371// // store all cluster features
372// for (const raster of Object.values(this.raster_stores)) {
373// const layerGrid = await raster.getTile(id);
374// if (layerGrid === undefined) continue;
375// const { name, size, data } = layerGrid;
376// const image = (data as RGBA[]).flatMap(({ r, g, b, a }) => [r, g, b, a]);
377// res.push({
378// name,
379// type: 'raw',
380// width: size,
381// height: size,
382// image: new Uint8Array(image),
383// });
384// }
385
386// if (res.length > 0) return res;
387// }
388
389// /**
390// * Get gridded data for a tile
391// *
392// * ## Parameters
393// * - `id`: the tile id
394// *
395// * ## Returns
396// * A collection of ImageDataInputs
397// */
398// async #getGridTile(id: S2CellId): Promise<GridInput[] | undefined> {
399// const res: GridInput[] = [];
400// // store all cluster features
401// for (const [layerName, grid] of Object.entries(this.grid_stores)) {
402// const { extent } = this.layer_guides.filter(
403// (guide) => guide.layerName === layerName,
404// )[0] as GridLayer;
405// const layerGrid = await grid.getTile(id);
406// if (layerGrid === undefined) continue;
407// const { name, size, data } = layerGrid;
408// res.push({
409// name,
410// size,
411// data: data as number[],
412// extent,
413// });
414// }
415
416// if (res.length > 0) return res;
417// }
418
419/// Get the absolute maxzoom from the layer guides
420///
421/// ## Parameters
422/// - `layer_guides`: user defined guide on building/shaping specific layer data
423///
424/// ## Returns
425/// Tthe absolute maxzoom
426fn get_zooms(layer_guides: &[LayerGuide]) -> (u8, u8) {
427 let mut min: u8 = 30;
428 let mut max = 0;
429
430 for layer_guide in layer_guides {
431 let (l_min, l_max) = layer_guide.zooms();
432 if l_min < min {
433 min = l_min;
434 }
435 if l_max > max {
436 max = l_max;
437 }
438 }
439
440 (min, max)
441}
442
443/// Pre-earclip polygons for faster processing of the tile client side
444///
445/// ## Parameters
446/// - `feature`: The feature to earclip. Only `Polygon` and `MultiPolygon` types are earclipped
447/// - `zoom`: The current zoom of the tile to ensure the tessellation is good for S2 projection
448pub fn earclip_polygons<M: Clone, P: MValueCompatible, D: MValueCompatible>(
449 feature: &mut VectorFeature<M, P, D>,
450 zoom: u8,
451) {
452 let mut polys: Vec<&VectorPolygon<D>> = vec![];
453 match &feature.geometry {
454 VectorGeometry::Polygon(poly) => {
455 polys.push(&poly.coordinates);
456 }
457 VectorGeometry::MultiPolygon(multipoly) => {
458 for poly in &multipoly.coordinates {
459 polys.push(poly);
460 }
461 }
462 _ => {}
463 }
464
465 let mut offset = 0;
466 let mut verts = vec![];
467 let mut indices = vec![];
468 for poly in polys {
469 // create triangle mesh
470 let (vertices, ind) = earclip(poly, None, Some(offset / 2));
471 // update vertex position
472 offset += vertices.len();
473 verts.extend(vertices);
474 // store indices
475 indices.extend(ind);
476 }
477 let tess_pos = verts.len();
478
479 // TODO: we can actually skip tesselate if our projection is not S2
480 let level = 1 << fmax(fmin(floor(zoom as f64 / 2.), 4.), 0.) as i32;
481 let division = (16 / level) as f64;
482 if division > 1. {
483 tesselate(&mut verts, &mut indices, 1. / division, 2);
484 }
485
486 let tess_points = &verts[tess_pos..];
487
488 // store
489 feature.geometry.set_indices(indices.into_iter().map(|i| i as u32).collect());
490 feature.geometry.set_tess(tess_points.to_vec());
491}