maplibre/vector/
process_vector.rs

1use std::{
2    borrow::Cow,
3    collections::{HashMap, HashSet},
4    marker::PhantomData,
5};
6
7use geozero::{
8    mvt::{tile, Message},
9    GeozeroDatasource,
10};
11use thiserror::Error;
12
13use crate::{
14    coords::WorldTileCoords,
15    io::{
16        apc::{Context, SendError},
17        geometry_index::{IndexProcessor, IndexedGeometry, TileIndex},
18    },
19    render::{
20        shaders::{ShaderSymbolVertex, ShaderSymbolVertexNew},
21        ShaderVertex,
22    },
23    sdf::{tessellation::TextTessellator, tessellation_new::TextTessellatorNew, Feature},
24    style::layer::{LayerPaint, StyleLayer},
25    vector::{
26        tessellation::{IndexDataType, OverAlignedVertexBuffer, ZeroTessellator},
27        transferables::{
28            LayerIndexed, LayerMissing, LayerTessellated, SymbolLayerTessellated, TileTessellated,
29            VectorTransferables,
30        },
31    },
32};
33
34#[derive(Error, Debug)]
35pub enum ProcessVectorError {
36    /// Sending of results failed
37    #[error("sending data back through context failed")]
38    SendError(SendError),
39    /// Error when decoding e.g. the protobuf file
40    #[error("decoding failed")]
41    Decoding(Cow<'static, str>),
42}
43
44/// A request for a tile at the given coordinates and in the given layers.
45pub struct VectorTileRequest {
46    pub coords: WorldTileCoords,
47    pub layers: HashSet<StyleLayer>,
48}
49
50/// Resolve the properties of an MVT feature into a HashMap of string key-value pairs,
51/// using the layer's keys/values dictionaries.
52fn resolve_feature_properties(
53    layer: &tile::Layer,
54    feature: &tile::Feature,
55) -> HashMap<String, String> {
56    let mut props = HashMap::new();
57    for pair in feature.tags.chunks(2) {
58        let [key_idx, value_idx] = [pair[0], pair[1]];
59        let Some(key) = layer.keys.get(key_idx as usize) else {
60            continue;
61        };
62        let Some(value) = layer.values.get(value_idx as usize) else {
63            continue;
64        };
65        let val_str = if let Some(ref v) = value.string_value {
66            v.clone()
67        } else if let Some(v) = value.float_value {
68            v.to_string()
69        } else if let Some(v) = value.double_value {
70            v.to_string()
71        } else if let Some(v) = value.int_value {
72            v.to_string()
73        } else if let Some(v) = value.uint_value {
74            v.to_string()
75        } else if let Some(v) = value.sint_value {
76            v.to_string()
77        } else if let Some(v) = value.bool_value {
78            v.to_string()
79        } else {
80            continue;
81        };
82        props.insert(key.clone(), val_str);
83    }
84    props
85}
86
87/// Evaluate a MapLibre GL JS legacy filter expression against feature properties.
88/// Supports: ["all", ...], ["any", ...], ["==", key, val], ["!=", key, val],
89/// ["has", key], ["!has", key], ["in", key, v1, v2, ...], ["!in", key, v1, v2, ...]
90fn evaluate_filter(filter: &serde_json::Value, props: &HashMap<String, String>) -> bool {
91    let Some(arr) = filter.as_array() else {
92        return true; // non-array filter passes everything
93    };
94    let Some(op) = arr.first().and_then(|v| v.as_str()) else {
95        return true;
96    };
97    match op {
98        "all" => arr[1..].iter().all(|f| evaluate_filter(f, props)),
99        "any" => arr[1..].iter().any(|f| evaluate_filter(f, props)),
100        "none" => !arr[1..].iter().any(|f| evaluate_filter(f, props)),
101        "==" if arr.len() >= 3 => {
102            let key = arr[1].as_str().unwrap_or("");
103            let expected = arr[2].as_str().map(|s| s.to_string()).unwrap_or_else(|| {
104                // Handle numeric comparisons
105                arr[2].as_f64().map(|n| n.to_string()).unwrap_or_default()
106            });
107            props.get(key).map(|v| v == &expected).unwrap_or(false)
108        }
109        "!=" if arr.len() >= 3 => {
110            let key = arr[1].as_str().unwrap_or("");
111            let expected = arr[2]
112                .as_str()
113                .map(|s| s.to_string())
114                .unwrap_or_else(|| arr[2].as_f64().map(|n| n.to_string()).unwrap_or_default());
115            props.get(key).map(|v| v != &expected).unwrap_or(true)
116        }
117        "has" if arr.len() >= 2 => {
118            let key = arr[1].as_str().unwrap_or("");
119            props.contains_key(key)
120        }
121        "!has" if arr.len() >= 2 => {
122            let key = arr[1].as_str().unwrap_or("");
123            !props.contains_key(key)
124        }
125        "in" if arr.len() >= 3 => {
126            let key = arr[1].as_str().unwrap_or("");
127            let Some(val) = props.get(key) else {
128                return false;
129            };
130            arr[2..]
131                .iter()
132                .any(|v| v.as_str().map(|s| s == val).unwrap_or(false))
133        }
134        "!in" if arr.len() >= 3 => {
135            let key = arr[1].as_str().unwrap_or("");
136            let Some(val) = props.get(key) else {
137                return true;
138            };
139            !arr[2..]
140                .iter()
141                .any(|v| v.as_str().map(|s| s == val).unwrap_or(false))
142        }
143        _ => {
144            log::warn!("unsupported filter operator: {op}");
145            true
146        }
147    }
148}
149
150/// Filter an MVT layer's features in-place according to a style filter expression.
151fn apply_filter_to_layer(layer: &mut tile::Layer, filter: &serde_json::Value) {
152    // Collect which features pass the filter (can't borrow layer immutably
153    // inside retain because retain borrows features mutably).
154    let keep: Vec<bool> = layer
155        .features
156        .iter()
157        .map(|feature| {
158            let props = resolve_feature_properties(layer, feature);
159            evaluate_filter(filter, &props)
160        })
161        .collect();
162    let mut idx = 0;
163    layer.features.retain(|_| {
164        let pass = keep[idx];
165        idx += 1;
166        pass
167    });
168}
169
170pub fn process_vector_tile<T: VectorTransferables, C: Context>(
171    data: &[u8],
172    tile_request: VectorTileRequest,
173    context: &mut ProcessVectorContext<T, C>,
174) -> Result<(), ProcessVectorError> {
175    let mut tile = geozero::mvt::Tile::decode(data)
176        .map_err(|e| ProcessVectorError::Decoding(e.to_string().into()))?;
177
178    // Report available layers
179    let coords = &tile_request.coords;
180
181    for style_layer in &tile_request.layers {
182        let id = &style_layer.id;
183        if let (Some(paint), Some(source_layer)) = (&style_layer.paint, &style_layer.source_layer) {
184            if let Some(layer) = tile
185                .layers
186                .iter_mut()
187                .find(|layer| &layer.name == source_layer)
188            {
189                // Clone the layer so filtering doesn't affect other style layers
190                // that reference the same source layer.
191                let mut filtered_layer = layer.clone();
192
193                // Apply style filter to exclude non-matching features
194                if let Some(filter) = &style_layer.filter {
195                    apply_filter_to_layer(&mut filtered_layer, filter);
196                }
197
198                let original_layer = filtered_layer.clone();
199                let layer = &mut filtered_layer;
200
201                match paint {
202                    LayerPaint::Line(_) | LayerPaint::Fill(_) => {
203                        let mut tessellator = ZeroTessellator::<IndexDataType>::default();
204                        match paint {
205                            LayerPaint::Fill(p) => {
206                                tessellator.style_property = p.fill_color.clone()
207                            }
208                            LayerPaint::Line(p) => {
209                                tessellator.style_property = p.line_color.clone();
210                                tessellator.is_line_layer = true;
211                            }
212                            LayerPaint::Background(p) => {
213                                tessellator.style_property = p.background_color.clone()
214                            }
215                            _ => {}
216                        }
217
218                        if let Err(e) = layer.process(&mut tessellator) {
219                            context.layer_missing(coords, &source_layer)?;
220
221                            tracing::error!("tesselation for layer source {source_layer} at {coords} failed {e:?}");
222                        } else {
223                            context.layer_tesselation_finished(
224                                coords,
225                                tessellator.buffer.into(),
226                                tessellator.feature_indices,
227                                tessellator.feature_colors,
228                                original_layer,
229                                id.clone(),
230                            )?;
231                        }
232                    }
233                    LayerPaint::Symbol(symbol_paint) => {
234                        let mut tessellator = TextTessellator::<IndexDataType>::default();
235                        let text_field = symbol_paint
236                            .text_field
237                            .clone()
238                            .unwrap_or_else(|| "name".to_string());
239                        let mut tessellator_new = TextTessellatorNew::new(text_field);
240
241                        if let Err(e) = layer.process(&mut tessellator_new) {
242                            context.layer_missing(coords, &source_layer)?;
243
244                            tracing::error!("tesselation for layer source {source_layer} at {coords} failed {e:?}");
245                        } else {
246                            tessellator_new.finish();
247                            context.symbol_layer_tesselation_finished(
248                                coords,
249                                tessellator.quad_buffer.into(),
250                                tessellator_new.quad_buffer.into(),
251                                tessellator_new.features,
252                                original_layer,
253                                id.clone(),
254                            )?;
255                        }
256                    }
257                    _ => {
258                        log::warn!("unhandled style layer type in {id}");
259                    }
260                }
261            } else {
262                log::warn!("layer source {source_layer} not found in vector tile");
263            }
264        } else {
265            log::error!("vector style layer {id} misses a required attribute");
266        }
267    }
268
269    // Report missing layers
270    let coords = &tile_request.coords;
271    let available_layers: HashSet<_> = tile
272        .layers
273        .iter()
274        .map(|layer| layer.name.clone())
275        .collect::<HashSet<_>>();
276
277    for layer in tile_request.layers {
278        if let Some(source_layer) = layer.source_layer {
279            if !available_layers.contains(&source_layer) {
280                context.layer_missing(coords, &source_layer)?;
281                tracing::info!(
282                    "requested source layer {source_layer} at {coords} not found in tile"
283                );
284            }
285        }
286    }
287
288    // Report index for layer
289    let mut index = IndexProcessor::new();
290
291    for layer in &mut tile.layers {
292        layer.process(&mut index).unwrap();
293    }
294
295    context.layer_indexing_finished(&tile_request.coords, index.get_geometries())?;
296
297    // Report end
298    tracing::info!("tile tessellated at {coords} finished");
299    context.tile_finished(coords)?;
300
301    Ok(())
302}
303
304pub struct ProcessVectorContext<T: VectorTransferables, C: Context> {
305    context: C,
306    phantom_t: PhantomData<T>,
307}
308
309impl<T: VectorTransferables, C: Context> ProcessVectorContext<T, C> {
310    pub fn new(context: C) -> Self {
311        Self {
312            context,
313            phantom_t: Default::default(),
314        }
315    }
316}
317
318impl<T: VectorTransferables, C: Context> ProcessVectorContext<T, C> {
319    pub fn take_context(self) -> C {
320        self.context
321    }
322
323    fn tile_finished(&mut self, coords: &WorldTileCoords) -> Result<(), ProcessVectorError> {
324        self.context
325            .send_back(T::TileTessellated::build_from(*coords))
326            .map_err(|e| ProcessVectorError::SendError(e))
327    }
328
329    fn layer_missing(
330        &mut self,
331        coords: &WorldTileCoords,
332        layer_name: &str,
333    ) -> Result<(), ProcessVectorError> {
334        self.context
335            .send_back(T::LayerMissing::build_from(*coords, layer_name.to_owned()))
336            .map_err(|e| ProcessVectorError::SendError(e))
337    }
338
339    fn layer_tesselation_finished(
340        &mut self,
341        coords: &WorldTileCoords,
342        buffer: OverAlignedVertexBuffer<ShaderVertex, IndexDataType>,
343        feature_indices: Vec<u32>,
344        feature_colors: Vec<[f32; 4]>,
345        layer_data: tile::Layer,
346        style_layer_id: String,
347    ) -> Result<(), ProcessVectorError> {
348        self.context
349            .send_back(T::LayerTessellated::build_from(
350                *coords,
351                buffer,
352                feature_indices,
353                feature_colors,
354                layer_data,
355                style_layer_id,
356            ))
357            .map_err(|e| ProcessVectorError::SendError(e))
358    }
359
360    fn symbol_layer_tesselation_finished(
361        &mut self,
362        coords: &WorldTileCoords,
363        buffer: OverAlignedVertexBuffer<ShaderSymbolVertex, IndexDataType>,
364        new_buffer: OverAlignedVertexBuffer<ShaderSymbolVertexNew, IndexDataType>,
365        features: Vec<Feature>,
366        layer_data: tile::Layer,
367        style_layer_id: String,
368    ) -> Result<(), ProcessVectorError> {
369        self.context
370            .send_back(T::SymbolLayerTessellated::build_from(
371                *coords,
372                buffer,
373                new_buffer,
374                features,
375                layer_data,
376                style_layer_id,
377            ))
378            .map_err(|e| ProcessVectorError::SendError(e))
379    }
380
381    fn layer_indexing_finished(
382        &mut self,
383        coords: &WorldTileCoords,
384        geometries: Vec<IndexedGeometry<f64>>,
385    ) -> Result<(), ProcessVectorError> {
386        self.context
387            .send_back(T::LayerIndexed::build_from(
388                *coords,
389                TileIndex::Linear { list: geometries },
390            ))
391            .map_err(|e| ProcessVectorError::SendError(e))
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::ProcessVectorContext;
398    use crate::{
399        coords::ZoomLevel,
400        io::apc::tests::DummyContext,
401        vector::{
402            process_vector::{process_vector_tile, VectorTileRequest},
403            DefaultVectorTransferables,
404        },
405    };
406
407    #[test] // TODO: Add proper tile byte array
408    #[ignore]
409    fn test() {
410        let _output = process_vector_tile(
411            &[0],
412            VectorTileRequest {
413                coords: (0, 0, ZoomLevel::default()).into(),
414                layers: Default::default(),
415            },
416            &mut ProcessVectorContext::<DefaultVectorTransferables, _>::new(DummyContext),
417        );
418    }
419}