maplibre/geojson/
mod.rs

1//! GeoJSON source processing — projects geographic coordinates into tile space and
2//! tessellates features using the existing vector rendering pipeline.
3
4use std::{borrow::Cow, f64::consts::PI};
5
6use geozero::{FeatureProcessor, GeomProcessor, GeozeroDatasource, PropertyProcessor};
7use thiserror::Error;
8
9use crate::{
10    coords::{WorldTileCoords, EXTENT},
11    io::apc::{Context, SendError},
12    sdf::{tessellation::TextTessellator, tessellation_new::TextTessellatorNew},
13    style::layer::{LayerPaint, StyleLayer},
14    vector::{
15        tessellation::{IndexDataType, ZeroTessellator},
16        transferables::{
17            LayerMissing, LayerTessellated, SymbolLayerTessellated, TileTessellated,
18            VectorTransferables,
19        },
20    },
21};
22
23#[derive(Error, Debug)]
24pub enum ProcessGeoJsonError {
25    #[error("sending data back through context failed")]
26    SendError(SendError),
27    #[error("GeoJSON parsing failed: {0}")]
28    Parse(Cow<'static, str>),
29}
30
31/// Wraps a processor and reprojects geographic (lon/lat) coordinates into
32/// tile-local extent coordinates (0–4096) using the Web Mercator projection.
33pub struct ProjectingTessellator<T> {
34    inner: T,
35    tile_x: i32,
36    tile_y: i32,
37    zoom: u8,
38    project: bool,
39}
40
41impl<T> ProjectingTessellator<T> {
42    pub fn new(coords: WorldTileCoords, project: bool, inner: T) -> Self {
43        Self {
44            inner,
45            tile_x: coords.x,
46            tile_y: coords.y,
47            zoom: u8::from(coords.z),
48            project,
49        }
50    }
51
52    /// Convert geographic lon/lat to tile-local extent coordinates (0–4096).
53    fn project(&self, lon: f64, lat: f64) -> (f64, f64) {
54        let lat = lat.clamp(-85.05112877980659, 85.05112877980659);
55        let scale = (1u64 << self.zoom) as f64;
56        let mx = (180.0 + lon) / 360.0;
57        let my = (180.0 - (180.0 / PI * ((PI / 4.0 + lat * PI / 360.0).tan()).ln())) / 360.0;
58        let x = (mx * scale - self.tile_x as f64) * EXTENT;
59        let y = (my * scale - self.tile_y as f64) * EXTENT;
60        (x, y)
61    }
62
63    pub fn into_inner(self) -> T {
64        self.inner
65    }
66}
67
68impl<T: GeomProcessor> GeomProcessor for ProjectingTessellator<T> {
69    fn xy(&mut self, x: f64, y: f64, idx: usize) -> geozero::error::Result<()> {
70        if x.is_nan() || y.is_nan() {
71            println!(
72                "ProjectingTessellator received NaN Input! x={}, y={}, idx={}",
73                x, y, idx
74            );
75        }
76        let (tx, ty) = self.project(x, y);
77        if !tx.is_finite() || !ty.is_finite() {
78            println!(
79                "ProjectingTessellator output non-finite! lon={}, lat={} -> tx={}, ty={}",
80                x, y, tx, ty
81            );
82        }
83        self.inner.xy(tx, ty, idx)
84    }
85
86    fn point_begin(&mut self, idx: usize) -> geozero::error::Result<()> {
87        self.inner.point_begin(idx)
88    }
89
90    fn point_end(&mut self, idx: usize) -> geozero::error::Result<()> {
91        self.inner.point_end(idx)
92    }
93
94    fn multipoint_begin(&mut self, size: usize, idx: usize) -> geozero::error::Result<()> {
95        self.inner.multipoint_begin(size, idx)
96    }
97
98    fn multipoint_end(&mut self, idx: usize) -> geozero::error::Result<()> {
99        self.inner.multipoint_end(idx)
100    }
101
102    fn linestring_begin(
103        &mut self,
104        tagged: bool,
105        size: usize,
106        idx: usize,
107    ) -> geozero::error::Result<()> {
108        self.inner.linestring_begin(tagged, size, idx)
109    }
110
111    fn linestring_end(&mut self, tagged: bool, idx: usize) -> geozero::error::Result<()> {
112        self.inner.linestring_end(tagged, idx)
113    }
114
115    fn multilinestring_begin(&mut self, size: usize, idx: usize) -> geozero::error::Result<()> {
116        self.inner.multilinestring_begin(size, idx)
117    }
118
119    fn multilinestring_end(&mut self, idx: usize) -> geozero::error::Result<()> {
120        self.inner.multilinestring_end(idx)
121    }
122
123    fn polygon_begin(
124        &mut self,
125        tagged: bool,
126        size: usize,
127        idx: usize,
128    ) -> geozero::error::Result<()> {
129        self.inner.polygon_begin(tagged, size, idx)
130    }
131
132    fn polygon_end(&mut self, tagged: bool, idx: usize) -> geozero::error::Result<()> {
133        self.inner.polygon_end(tagged, idx)
134    }
135
136    fn multipolygon_begin(&mut self, size: usize, idx: usize) -> geozero::error::Result<()> {
137        self.inner.multipolygon_begin(size, idx)
138    }
139
140    fn multipolygon_end(&mut self, idx: usize) -> geozero::error::Result<()> {
141        self.inner.multipolygon_end(idx)
142    }
143}
144
145impl<T: PropertyProcessor> PropertyProcessor for ProjectingTessellator<T> {
146    fn property(
147        &mut self,
148        idx: usize,
149        name: &str,
150        value: &geozero::ColumnValue,
151    ) -> geozero::error::Result<bool> {
152        self.inner.property(idx, name, value)
153    }
154}
155
156impl<T: FeatureProcessor> FeatureProcessor for ProjectingTessellator<T> {
157    fn dataset_begin(&mut self, name: Option<&str>) -> geozero::error::Result<()> {
158        self.inner.dataset_begin(name)
159    }
160    fn dataset_end(&mut self) -> geozero::error::Result<()> {
161        self.inner.dataset_end()
162    }
163    fn feature_begin(&mut self, idx: u64) -> geozero::error::Result<()> {
164        self.inner.feature_begin(idx)
165    }
166    fn properties_begin(&mut self) -> geozero::error::Result<()> {
167        self.inner.properties_begin()
168    }
169    fn properties_end(&mut self) -> geozero::error::Result<()> {
170        self.inner.properties_end()
171    }
172    fn geometry_begin(&mut self) -> geozero::error::Result<()> {
173        self.inner.geometry_begin()
174    }
175    fn geometry_end(&mut self) -> geozero::error::Result<()> {
176        self.inner.geometry_end()
177    }
178    fn feature_end(&mut self, idx: u64) -> geozero::error::Result<()> {
179        self.inner.feature_end(idx)
180    }
181}
182
183/// Request for processing GeoJSON features for a set of style layers.
184pub struct GeoJsonTileRequest {
185    pub coords: WorldTileCoords,
186    pub layers: Vec<StyleLayer>,
187    /// Name of the GeoJSON source (used to match style layers by `source` field).
188    pub source_name: String,
189    /// If true, applies Web Mercator projection. Tests use false.
190    pub project: bool,
191}
192
193/// Process inline GeoJSON data and tessellate features for each matching style layer.
194///
195/// This mirrors [`crate::vector::process_vector_tile`] but works with geographic
196/// (lon/lat) coordinates rather than pre-projected MVT tile coordinates.
197///
198/// For each style layer that references the named GeoJSON source (and has no
199/// `source_layer`), ALL features in the GeoJSON are tessellated and sent back
200/// via `context`. The tessellated bucket's virtual source-layer name is set to
201/// `style_layer.id`, matching the fallback in `upload_system`.
202pub fn process_geojson_features<T: VectorTransferables, C: Context>(
203    geojson_value: &serde_json::Value,
204    request: GeoJsonTileRequest,
205    context: &C,
206) -> Result<(), ProcessGeoJsonError> {
207    let coords = request.coords;
208    let json_str = geojson_value.to_string();
209
210    for style_layer in &request.layers {
211        let matches_source = style_layer
212            .source
213            .as_deref()
214            .map_or(false, |s| s == request.source_name);
215        if !matches_source {
216            continue;
217        }
218
219        let Some(paint) = &style_layer.paint else {
220            log::warn!("GeoJSON style layer {} has no paint", style_layer.id);
221            continue;
222        };
223
224        match paint {
225            LayerPaint::Fill(_) | LayerPaint::Line(_) | LayerPaint::Background(_) => {
226                let mut tessellator = ZeroTessellator::<IndexDataType>::default();
227                match paint {
228                    LayerPaint::Fill(p) => tessellator.style_property = p.fill_color.clone(),
229                    LayerPaint::Line(p) => {
230                        tessellator.style_property = p.line_color.clone();
231                        tessellator.is_line_layer = true;
232                    }
233                    LayerPaint::Background(p) => {
234                        tessellator.style_property = p.background_color.clone()
235                    }
236                    _ => {}
237                }
238
239                let mut projecting =
240                    ProjectingTessellator::new(coords, request.project, tessellator);
241
242                let mut geojson_src = geozero::geojson::GeoJson(json_str.as_str());
243                if let Err(e) = geojson_src.process(&mut projecting) {
244                    log::warn!(
245                        "GeoJSON tessellation for layer {} failed: {e:?}",
246                        style_layer.id
247                    );
248                    context
249                        .send_back(T::LayerMissing::build_from(coords, style_layer.id.clone()))
250                        .map_err(ProcessGeoJsonError::SendError)?;
251                    continue;
252                }
253
254                let mut inner = projecting.into_inner();
255                // For bare GeoJSON geometries (Polygon, LineString, etc. — not a
256                // FeatureCollection), geozero never calls `feature_end`, so
257                // `feature_indices` stays empty while `buffer.indices` is not.
258                // Manually commit the remaining geometry as a single feature.
259                if inner.feature_indices.is_empty() && !inner.buffer.indices.is_empty() {
260                    let _ = inner.feature_end(0);
261                }
262
263                let synthetic_layer = geozero::mvt::tile::Layer {
264                    version: 2,
265                    name: style_layer.id.clone(),
266                    ..Default::default()
267                };
268
269                context
270                    .send_back(T::LayerTessellated::build_from(
271                        coords,
272                        inner.buffer.into(),
273                        inner.feature_indices,
274                        inner.feature_colors,
275                        synthetic_layer,
276                        style_layer.id.clone(),
277                    ))
278                    .map_err(ProcessGeoJsonError::SendError)?;
279            }
280            LayerPaint::Symbol(symbol_paint) => {
281                let mut tessellator = TextTessellator::<IndexDataType>::default();
282                let text_field = symbol_paint
283                    .text_field
284                    .clone()
285                    .unwrap_or_else(|| "name".to_string());
286                let mut tessellator_new = TextTessellatorNew::new(text_field);
287                let mut projecting =
288                    ProjectingTessellator::new(coords, request.project, tessellator_new);
289
290                let mut geojson_src = geozero::geojson::GeoJson(json_str.as_str());
291                if let Err(e) = geojson_src.process(&mut projecting) {
292                    log::warn!(
293                        "GeoJSON text tessellation for layer {} failed: {e:?}",
294                        style_layer.id
295                    );
296                    context
297                        .send_back(T::LayerMissing::build_from(coords, style_layer.id.clone()))
298                        .map_err(ProcessGeoJsonError::SendError)?;
299                    continue;
300                }
301
302                let mut inner = projecting.into_inner();
303                inner.finish();
304
305                let synthetic_layer = geozero::mvt::tile::Layer {
306                    version: 2,
307                    name: style_layer.id.clone(),
308                    ..Default::default()
309                };
310
311                context
312                    .send_back(T::SymbolLayerTessellated::build_from(
313                        coords,
314                        tessellator.quad_buffer.into(),
315                        inner.quad_buffer.into(),
316                        inner.features,
317                        synthetic_layer,
318                        style_layer.id.clone(),
319                    ))
320                    .map_err(ProcessGeoJsonError::SendError)?;
321            }
322            _ => {
323                log::trace!(
324                    "GeoJSON layer {} has unsupported paint type, skipping",
325                    style_layer.id
326                );
327            }
328        }
329    }
330
331    context
332        .send_back(T::TileTessellated::build_from(coords))
333        .map_err(ProcessGeoJsonError::SendError)?;
334
335    Ok(())
336}