gistools/writers/to_tiles/
mod.rs

1/// Types, Structs, and Enums for the to_tiles converter
2pub mod types;
3/// Worker
4pub mod worker;
5
6use super::OnFeature;
7#[cfg(feature = "std")]
8use crate::writers::FileTileWriter;
9use crate::{
10    data_structures::HasLayer,
11    geometry::{wm, xyz_to_bbox},
12    parsers::FeatureReader,
13    writers::{LocalTileWriter, TileWriter},
14};
15use alloc::{string::String, vec::Vec};
16use core::mem::take;
17use s2_tilejson::{LonLatBounds, MetadataBuilder, Scheme};
18use s2json::{MValueCompatible, Projection};
19pub use types::*;
20use worker::{BuiltTile, TileWorker};
21
22/// Use a Local Tile Builder via memory
23///
24/// See [`TileBuilder`] for full documentation
25pub type LocalTileBuilder = TileBuilder<LocalTileWriter>;
26/// Use the Filesystem to store all file data
27///
28/// See [`TileBuilder`] for full documentation
29#[cfg(feature = "std")]
30pub type FileTileBuilder = TileBuilder<FileTileWriter>;
31
32/// # The Tile Builder
33///
34/// ## Description
35/// The TileBuilder creates tiles for the user given any source reader that implements [`FeatureReader`]
36///
37/// Create vector tiles, raster tiles, or gridded tiles.
38///
39/// Store as Mapbox Vector Tiles or Open Vector Tiles. Utilizes the [`TileWriter`] trait to write the data to.
40///
41/// Supports storing as a folder structure or a PMTiles (with gzip compression if enabled)
42///
43/// ## Usage
44///
45/// The methods you have access to:
46/// - [`TileBuilder::new`]: Create a new TileBuilder
47/// - [`TileBuilder::tile_writer`]: Get the tile writer
48/// - [`TileBuilder::add_vector_source`]: Add a vector source to tile-ize
49/// - [`TileBuilder::add_grid_source`]: Add data that will be gridded like raster data but float precision points
50/// - [`TileBuilder::build_tiles`]: After adding all the source data, build all tiles into the tile writer
51///
52/// The Tile Writers this library supports:
53/// - [`LocalTileWriter`]
54/// - [`FileTileWriter`]
55/// - [`crate::writers::PMTilesWriter`]
56/// - [`crate::writers::S2TilesWriter`]
57///
58/// ### Writing WM projection tiles as Mapbox Vector Tiles to a PMTiles file
59///
60/// ```rust
61/// use gistools::{
62///     data_structures::TileStoreOptions,
63///     parsers::{BufferWriter, FileReader},
64///     readers::JSONReader,
65///     util::CompressionFormat,
66///     writers::{
67///         BuildGuide, PMTilesWriter, TileBuilder, LayerGuide,
68///         VectorLayerGuide, BaseLayer
69///     }
70/// };
71/// use serde::{Deserialize, Serialize};
72/// use s2_tilejson::{Metadata, MetadataBuilder, DrawType};
73/// use open_vector_tile::Extent;
74/// use s2json::{MValue, MValueCompatible, Properties, Projection};
75/// use std::{path::PathBuf, collections::BTreeMap};
76///
77/// #[derive(Debug, Default, Clone, MValueCompatible, PartialEq, Serialize, Deserialize)]
78/// #[serde(default)]
79/// struct PointProps {
80///     id: i64,
81/// }
82///
83/// #[derive(Debug, Default, Clone, MValueCompatible, PartialEq, Serialize, Deserialize)]
84/// #[serde(default)]
85/// struct LineProps {
86///     linename: String,
87/// }
88///
89/// #[derive(Debug, Default, Clone, MValueCompatible, PartialEq, Serialize, Deserialize)]
90/// #[serde(default)]
91/// struct PolyProps {
92///     poly3d: bool,
93/// }
94///
95/// // using a buffer writer for example but ideally you are using a FileWriter
96/// let tmp_buffer_writer = BufferWriter::new(vec![]);
97/// let pm_writer = PMTilesWriter::new(tmp_buffer_writer, CompressionFormat::None);
98/// let build_guide = BuildGuide {
99///     projection: Projection::WG,
100///     build_indices: true,
101///     attributions: BTreeMap::from([("Satellite Data".into(), "https://example.com".into())]),
102///     layer_guides: vec![
103///         // add points
104///         LayerGuide::Vector(VectorLayerGuide {
105///             extent: Extent::Extent4096,
106///             draw_types: vec![DrawType::Points, DrawType::Points3D],
107///             shape: Some((&MValue::from(PointProps::default())).into()),
108///             base: BaseLayer {
109///                 description: Some("Points Vector Layer".into()),
110///                 source_name: "all_features".into(),
111///                 layer_name: "points".into(),
112///             },
113///             vector_guide: TileStoreOptions { maxzoom: Some(4), ..Default::default() },
114///             ..Default::default()
115///         }),
116///         // add lines
117///         LayerGuide::Vector(VectorLayerGuide {
118///             extent: Extent::Extent4096,
119///             draw_types: vec![DrawType::Lines, DrawType::Lines3D],
120///             shape: Some((&MValue::from(LineProps::default())).into()),
121///             base: BaseLayer {
122///                 description: Some("Lines Vector Layer".into()),
123///                 source_name: "all_features".into(),
124///                 layer_name: "lines".into(),
125///             },
126///             vector_guide: TileStoreOptions { maxzoom: Some(4), ..Default::default() },
127///             ..Default::default()
128///         }),
129///         // add polys
130///         LayerGuide::Vector(VectorLayerGuide {
131///             extent: Extent::Extent4096,
132///             draw_types: vec![DrawType::Polygons, DrawType::Polygons3D],
133///             shape: Some((&MValue::from(PolyProps::default())).into()),
134///             base: BaseLayer {
135///                 description: Some("Polygons Vector Layer".into()),
136///                 source_name: "all_features".into(),
137///                 layer_name: "polys".into(),
138///             },
139///             vector_guide: TileStoreOptions { maxzoom: Some(4), ..Default::default() },
140///             ..Default::default()
141///         }),
142///     ],
143///     ..Default::default()
144/// };
145/// let mut tile_builder = TileBuilder::new(pm_writer, build_guide);
146///
147/// // add json features
148/// let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
149/// path = path.join("tests/writers/fixtures/all-features.json");
150/// let reader: JSONReader<FileReader, (), Properties, Properties> =
151///     JSONReader::new(FileReader::from(path));
152/// tile_builder.add_vector_source("all_features".into(), reader, None, None);
153///
154/// // build
155/// tile_builder.build_tiles();
156/// ```
157///
158/// ## Links
159/// - <https://github.com/Open-S2/s2-pmtiles>
160/// - <https://github.com/protomaps/PMTiles>
161/// - <https://github.com/mapbox/vector-tile-spec>
162#[derive(Debug)]
163pub struct TileBuilder<W: TileWriter = LocalTileWriter> {
164    /// The data created will be stored in either a folder structure or a pmtiles file
165    /// Folder structure is either '{face}/{zoom}/{x}/{y}.pbf' or '{zoom}/{x}/{y}.pbf'.
166    /// PMTiles store all data in a single data file.
167    tile_writer: W,
168    /// Explains how to build data
169    build_guide: BuildGuide,
170    /// TileWorker collects data then builds tiles
171    worker: TileWorker,
172    /// The metadata that will be stored with the tile data
173    meta_builder: MetadataBuilder,
174}
175impl<W: TileWriter> TileBuilder<W> {
176    /// Create a new Tile builder
177    pub fn new(tile_writer: W, build_guide: BuildGuide) -> Self {
178        let meta_builder = setup_builder(&build_guide);
179        let worker = TileWorker::new(0, build_guide.clone());
180        Self { tile_writer, build_guide, worker, meta_builder }
181    }
182
183    /// Get the tile writer
184    pub fn tile_writer(&self) -> &W {
185        &self.tile_writer
186    }
187
188    /// Add a vector source to tile-ize
189    pub fn add_vector_source<
190        M: Clone + HasLayer,
191        P: MValueCompatible,
192        D: MValueCompatible,
193        T: FeatureReader<M, P, D>,
194    >(
195        &mut self,
196        source_name: String,
197        reader: T,
198        on_source_feature: Option<OnFeature<M, P, D>>,
199        on_layer_feature: Option<&Vec<LayerHandler<M, P, D>>>,
200    ) {
201        for mut feature in reader.iter() {
202            if let Some(on_feature) = on_source_feature
203                && let Some(new_feature) = (on_feature)(take(&mut feature))
204            {
205                feature = new_feature;
206            }
207            self.worker.store_feature(feature, source_name.clone(), on_layer_feature);
208        }
209    }
210
211    /// Add vector points with RGBA attributes to build raster tiles
212    #[cfg_attr(feature = "nightly", coverage(off))] // not implemented don't punish
213    pub fn add_raster_source<
214        M: Clone,
215        P: MValueCompatible,
216        D: MValueCompatible,
217        T: FeatureReader<M, P, D>,
218    >(
219        _source_name: String,
220        _reader: T,
221        _on_feature: Option<OnFeature<M, P, D>>,
222    ) {
223        unimplemented!()
224    }
225
226    /// Add data that will be gridded like raster data but float precision points
227    #[cfg_attr(feature = "nightly", coverage(off))] // not implemented don't punish
228    pub fn add_grid_source<
229        M: Clone,
230        P: MValueCompatible,
231        D: MValueCompatible,
232        T: FeatureReader<M, P, D>,
233    >(
234        _source_name: String,
235        _reader: T,
236        _on_feature: Option<OnFeature<M, P, D>>,
237    ) {
238        unimplemented!()
239    }
240
241    /// After adding all the source data, build all tiles into the tile writer
242    pub fn build_tiles(&mut self) {
243        let BuildGuide { projection, .. } = &self.build_guide;
244        // ensure all data is sorted
245        self.worker.sort();
246        // collect all tiles
247        for BuiltTile { face, zoom, x, y, data } in self.worker.build_tiles() {
248            if *projection == Projection::S2 {
249                self.tile_writer.write_tile_s2(face, zoom, x, y, data);
250                // TODO: Get correct ll-bounds
251                self.meta_builder.add_tile_s2(face, zoom, x, y, &LonLatBounds::default());
252            } else {
253                self.tile_writer.write_tile_wm(zoom, x, y, data);
254                let (w, s, e, n) =
255                    xyz_to_bbox(x, y, zoom, Some(false), Some(wm::Source::WGS84), None);
256                self.meta_builder.add_tile_wm(zoom, x, y, &LonLatBounds::new(w, s, e, n));
257            }
258        }
259        // finally commit the metadata
260        self.tile_writer
261            .commit(self.meta_builder.commit(), Some((&self.build_guide.encoding).into()));
262    }
263}
264
265/// Setup a metadata builder
266fn setup_builder(build_guide: &BuildGuide) -> MetadataBuilder {
267    let mut meta_builder = MetadataBuilder::default();
268    let BuildGuide { name, description, version, projection, attributions, .. } = build_guide;
269
270    meta_builder.set_name(name.into());
271    meta_builder.set_extension(build_guide.extension.clone());
272    meta_builder.set_description(description.into());
273    meta_builder.set_version(version.into());
274    // NOTE: For now we only support xyz and fzxy
275    let scheme = if *projection == Projection::S2 { Scheme::Fzxy } else { Scheme::Xyz };
276    meta_builder.set_scheme(scheme);
277    meta_builder.set_type((&build_guide.format).into());
278
279    meta_builder.set_encoding(build_guide.encoding.clone());
280    // Add attribution
281    for (display_name, href) in attributions.iter() {
282        meta_builder.add_attribution(display_name, href);
283    }
284    // add layer guides
285    for layer in build_guide.layer_guides.iter() {
286        meta_builder.add_layer(layer.layer_name(), &layer.into());
287    }
288
289    meta_builder
290}