Skip to main content

rustial_engine/map_state/
picking.rs

1use super::*;
2
3impl MapState {
4    /// Execute a pick operation against the current map state.
5    pub fn pick(&self, query: PickQuery, options: PickOptions) -> PickResult {
6        let (query_coord, _ray) = self.resolve_pick_query(&query);
7        let mut result = PickResult {
8            hits: Vec::new(),
9            query_coord,
10            projection: Some(self.camera.projection()),
11        };
12        if options.include_terrain_surface {
13            if let Some(coord) = &query_coord {
14                let elevation = self.terrain.elevation_at(coord);
15                result.hits.push(PickHit::terrain_surface(*coord, elevation));
16            }
17        }
18        if let Some(coord) = &query_coord {
19            let tolerance_meters = if options.tolerance_meters > 0.0 {
20                options.tolerance_meters
21            } else {
22                self.camera.meters_per_pixel() * 8.0
23            };
24            self.pick_features_at_geo(coord, &options, tolerance_meters, &mut result.hits);
25        }
26        result.hits.sort_by(|a, b| {
27            a.layer_priority
28                .cmp(&b.layer_priority)
29                .then_with(|| a.distance_meters.total_cmp(&b.distance_meters))
30        });
31        if options.limit > 0 && result.hits.len() > options.limit {
32            result.hits.truncate(options.limit);
33        }
34        result
35    }
36
37    /// Pick using logical screen-space coordinates relative to the viewport.
38    ///
39    /// This is a convenience wrapper around [`Self::pick`] for the common
40    /// cursor-driven interaction path used by hover and click handling.
41    #[inline]
42    pub fn pick_at_screen(&self, x: f64, y: f64, options: PickOptions) -> PickResult {
43        self.pick(PickQuery::screen(x, y), options)
44    }
45
46    /// Pick using a geographic coordinate.
47    ///
48    /// This is a convenience wrapper around [`Self::pick`] for workflows that
49    /// already resolved the target point in geospatial coordinates.
50    #[inline]
51    pub fn pick_at_geo(&self, coord: GeoCoord, options: PickOptions) -> PickResult {
52        self.pick(PickQuery::geo(coord), options)
53    }
54
55    /// Pick along a world-space ray in the active scene projection.
56    ///
57    /// This is a convenience wrapper around [`Self::pick`] for renderers or
58    /// host integrations that already computed a world-space interaction ray.
59    #[inline]
60    pub fn pick_along_ray(
61        &self,
62        origin: glam::DVec3,
63        direction: glam::DVec3,
64        options: PickOptions,
65    ) -> PickResult {
66        self.pick(PickQuery::ray(origin, direction), options)
67    }
68
69    /// Query rendered vector features near the given query position.
70    pub fn query_rendered_features(&self, query: PickQuery, options: QueryOptions) -> Vec<QueriedFeature> {
71        let (query_coord, _ray) = self.resolve_pick_query(&query);
72        let Some(coord) = query_coord else {
73            return Vec::new();
74        };
75
76        let tolerance_meters = if options.tolerance_meters > 0.0 {
77            options.tolerance_meters
78        } else {
79            self.camera.meters_per_pixel() * 8.0
80        };
81
82        self.query_vector_features_at_geo(&coord, &options, tolerance_meters)
83    }
84
85    /// Query rendered features at a logical screen-space coordinate.
86    ///
87    /// This is the convenience wrapper for cursor-driven workflows that already
88    /// operate in viewport pixel coordinates.
89    #[inline]
90    pub fn query_rendered_features_at_screen(
91        &self,
92        x: f64,
93        y: f64,
94        options: QueryOptions,
95    ) -> Vec<QueriedFeature> {
96        self.query_rendered_features(PickQuery::screen(x, y), options)
97    }
98
99    /// Query rendered features at a geographic coordinate.
100    ///
101    /// This is the convenience wrapper for callers that already resolved the
102    /// interaction point in geospatial coordinates.
103    #[inline]
104    pub fn query_rendered_features_at_geo(
105        &self,
106        coord: GeoCoord,
107        options: QueryOptions,
108    ) -> Vec<QueriedFeature> {
109        self.query_rendered_features(PickQuery::geo(coord), options)
110    }
111
112    /// Query rendered features along a world-space ray.
113    ///
114    /// This is the convenience wrapper for renderer integrations that already
115    /// computed a world-space interaction ray and want the standard rendered
116    /// feature query behavior.
117    #[inline]
118    pub fn query_rendered_features_along_ray(
119        &self,
120        origin: glam::DVec3,
121        direction: glam::DVec3,
122        options: QueryOptions,
123    ) -> Vec<QueriedFeature> {
124        self.query_rendered_features(PickQuery::ray(origin, direction), options)
125    }
126
127    /// Query rendered vector features whose geometry intersects a screen-space
128    /// rectangle defined by two corner points.
129    ///
130    /// The two corners are resolved to geographic coordinates via the current
131    /// camera projection and then all visible features whose geometry overlaps
132    /// the resulting bounding box are returned.
133    ///
134    /// This is the Rustial equivalent of the Mapbox / MapLibre
135    /// `queryRenderedFeatures([sw, ne])` overload that accepts a bounding box
136    /// instead of a single point.
137    ///
138    /// # Arguments
139    ///
140    /// - `x1`, `y1` -- first corner in logical viewport pixels.
141    /// - `x2`, `y2` -- second corner in logical viewport pixels.
142    /// - `options` -- layer/source filtering and tolerance controls.
143    ///
144    /// # Sorting
145    ///
146    /// Results are sorted by layer-stack order (top-most layer first) and
147    /// de-duplicated so the same feature is not returned twice from
148    /// overlapping streamed tiles.
149    pub fn query_rendered_features_in_box(
150        &self,
151        x1: f64,
152        y1: f64,
153        x2: f64,
154        y2: f64,
155        options: QueryOptions,
156    ) -> Vec<QueriedFeature> {
157        // Resolve both screen corners to geo coordinates.
158        let geo1 = self.camera.screen_to_geo(x1, y1);
159        let geo2 = self.camera.screen_to_geo(x2, y2);
160        let (Some(a), Some(b)) = (geo1, geo2) else {
161            return Vec::new();
162        };
163        self.query_vector_features_in_bbox(&a, &b, &options)
164    }
165
166    /// Query currently loaded features for a style source.
167    pub fn query_source_features(
168        &self,
169        source_id: &str,
170        source_layer: Option<&str>,
171    ) -> Vec<QueriedFeature> {
172        use crate::layer::LayerKind;
173        use crate::layers::VectorLayer;
174
175        let mut out = Vec::new();
176        let mut seen = HashSet::new();
177
178        for layer in self.layers.iter() {
179            if !layer.visible() || layer.kind() != LayerKind::Vector {
180                continue;
181            }
182
183            let Some(vector_layer) = layer.as_any().downcast_ref::<VectorLayer>() else {
184                continue;
185            };
186
187            let layer_source_id = vector_layer
188                .query_source_id
189                .as_deref()
190                .unwrap_or(layer.name());
191            if layer_source_id != source_id {
192                continue;
193            }
194
195            let layer_id = vector_layer
196                .query_layer_id
197                .as_deref()
198                .unwrap_or(layer.name());
199
200            let streamed_payloads = self.streamed_payload_view_for(layer_id);
201            if !streamed_payloads.is_empty() {
202                for entry in streamed_payloads.iter() {
203                    let feature_source_layer =
204                        entry.source_layer(vector_layer.query_source_layer.as_deref());
205                    if let Some(requested_source_layer) = source_layer {
206                        if feature_source_layer != Some(requested_source_layer) {
207                            continue;
208                        }
209                    }
210
211                    let seen_key = (
212                        feature_source_layer.map(str::to_owned),
213                        format!("{:?}", entry.feature.geometry),
214                    );
215                    if !seen.insert(seen_key) {
216                        continue;
217                    }
218
219                    out.push(QueriedFeature {
220                        layer_id: None,
221                        source_id: Some(source_id.to_owned()),
222                        source_layer: feature_source_layer.map(str::to_owned),
223                        source_tile: entry.source_tile(),
224                        feature_id: entry.feature.feature_id.clone(),
225                        feature_index: entry.feature.feature_index,
226                        geometry: entry.feature.geometry.clone(),
227                        properties: entry.feature.properties.clone(),
228                        state: self
229                            .feature_state
230                            .get(&FeatureStateId::new(source_id, &entry.feature.feature_id))
231                            .cloned()
232                            .unwrap_or_default(),
233                        distance_meters: 0.0,
234                        from_symbol: false,
235                    });
236                }
237                continue;
238            }
239
240            for (idx, feature) in vector_layer.features.features.iter().enumerate() {
241                let provenance = vector_layer
242                    .feature_provenance
243                    .get(idx)
244                    .and_then(|entry| entry.as_ref());
245                let feature_source_layer = provenance
246                    .and_then(|entry| entry.source_layer.as_deref())
247                    .or(vector_layer.query_source_layer.as_deref());
248                if let Some(requested_source_layer) = source_layer {
249                    if feature_source_layer != Some(requested_source_layer) {
250                        continue;
251                    }
252                }
253
254                let feature_id = feature_id_for_feature(feature, idx);
255                let seen_key = (
256                    feature_source_layer.map(str::to_owned),
257                    format!("{:?}", feature.geometry),
258                );
259                if !seen.insert(seen_key) {
260                    continue;
261                }
262
263                out.push(QueriedFeature {
264                    layer_id: None,
265                    source_id: Some(source_id.to_owned()),
266                    source_layer: feature_source_layer.map(str::to_owned),
267                    source_tile: provenance.and_then(|entry| entry.source_tile),
268                    feature_id: feature_id.clone(),
269                    feature_index: idx,
270                    geometry: feature.geometry.clone(),
271                    properties: feature.properties.clone(),
272                    state: self
273                        .feature_state
274                        .get(&FeatureStateId::new(source_id, &feature_id))
275                        .cloned()
276                        .unwrap_or_default(),
277                    distance_meters: 0.0,
278                    from_symbol: false,
279                });
280            }
281        }
282
283        out
284    }
285
286    /// Resolve a PickQuery into a geographic coordinate and optional ray.
287    fn resolve_pick_query(&self, query: &PickQuery) -> (Option<GeoCoord>, Option<(glam::DVec3, glam::DVec3)>) {
288        match query {
289            PickQuery::Screen { x, y } => {
290                let coord = self.screen_to_geo(*x, *y);
291                let ray = Some(self.camera.screen_to_ray(*x, *y));
292                (coord, ray)
293            }
294            PickQuery::Geo { coord } => (Some(*coord), None),
295            PickQuery::Ray { origin, direction } => {
296                let coord = self.ray_to_geo(*origin, *direction);
297                (coord, Some((*origin, *direction)))
298            }
299        }
300    }
301
302    fn query_vector_features_at_geo(
303        &self,
304        coord: &GeoCoord,
305        options: &QueryOptions,
306        tolerance_meters: f64,
307    ) -> Vec<QueriedFeature> {
308        use crate::layer::LayerKind;
309        use crate::layers::VectorLayer;
310
311        let filter_layers: Option<HashSet<&str>> = if options.layers.is_empty() {
312            None
313        } else {
314            Some(options.layers.iter().map(String::as_str).collect())
315        };
316        let filter_sources: Option<HashSet<&str>> = if options.sources.is_empty() {
317            None
318        } else {
319            Some(options.sources.iter().map(String::as_str).collect())
320        };
321
322        let mut out = Vec::new();
323        let mut seen_streamed = HashSet::new();
324
325        for layer in self.layers.iter() {
326            if !layer.visible() || layer.kind() != LayerKind::Vector {
327                continue;
328            }
329
330            let layer_name = layer.name();
331            let Some(vector_layer) = layer.as_any().downcast_ref::<VectorLayer>() else {
332                continue;
333            };
334
335            let query_layer_id = vector_layer
336                .query_layer_id
337                .as_deref()
338                .unwrap_or(layer_name);
339            if let Some(ref filtered_layers) = filter_layers {
340                if !filtered_layers.contains(query_layer_id) && !filtered_layers.contains(layer_name) {
341                    continue;
342                }
343            }
344
345            let source_id = vector_layer
346                .query_source_id
347                .as_deref()
348                .unwrap_or(layer_name);
349            if let Some(ref filtered_sources) = filter_sources {
350                if !filtered_sources.contains(source_id) {
351                    continue;
352                }
353            }
354
355            if options.include_symbols
356                && vector_layer.style.render_mode == crate::layers::VectorRenderMode::Symbol
357            {
358                if let Some(symbol_payloads) = self.streamed_symbol_query_payloads.get(query_layer_id) {
359                    let mut seen_symbols = HashSet::new();
360                    for payload in symbol_payloads {
361                        for symbol in &payload.symbols {
362                            if let Some(distance_meters) = symbol_hit_distance_at_geo(
363                                symbol,
364                                coord,
365                                self.camera.projection(),
366                            ) {
367                                let seen_key = (
368                                    source_id.to_owned(),
369                                    symbol.source_layer.clone(),
370                                    symbol.feature_id.clone(),
371                                );
372                                if !seen_symbols.insert(seen_key) {
373                                    continue;
374                                }
375
376                                out.push(QueriedFeature {
377                                    layer_id: Some(query_layer_id.to_owned()),
378                                    source_id: Some(source_id.to_owned()),
379                                    source_layer: symbol
380                                        .source_layer
381                                        .clone()
382                                        .or_else(|| vector_layer.query_source_layer.clone()),
383                                    source_tile: payload.tile.or(symbol.source_tile),
384                                    feature_id: symbol.feature_id.clone(),
385                                    feature_index: symbol.feature_index,
386                                    geometry: crate::geometry::Geometry::Point(crate::geometry::Point {
387                                        coord: symbol.anchor,
388                                    }),
389                                    properties: HashMap::new(),
390                                    state: self
391                                        .feature_state
392                                        .get(&FeatureStateId::new(source_id, &symbol.feature_id))
393                                        .cloned()
394                                        .unwrap_or_default(),
395                                    distance_meters,
396                                    from_symbol: true,
397                                });
398                            }
399                        }
400                    }
401                    continue;
402                }
403            }
404
405            if !options.include_symbols
406                && vector_layer.style.render_mode == crate::layers::VectorRenderMode::Symbol
407            {
408                continue;
409            }
410
411            let streamed_payloads = self.streamed_payload_view_for(query_layer_id);
412            if !streamed_payloads.is_empty() {
413                for entry in streamed_payloads.iter() {
414                    let feature_source_layer = entry
415                        .source_layer(vector_layer.query_source_layer.as_deref())
416                        .map(str::to_owned);
417                    if let Some(distance_meters) = geometry_hit_distance(
418                        &entry.feature.geometry,
419                        coord,
420                        tolerance_meters,
421                    ) {
422                        let seen_key = (
423                            source_id.to_owned(),
424                            feature_source_layer.clone(),
425                            format!("{:?}", entry.feature.geometry),
426                        );
427                        if !seen_streamed.insert(seen_key) {
428                            continue;
429                        }
430
431                        out.push(QueriedFeature {
432                            layer_id: Some(query_layer_id.to_owned()),
433                            source_id: Some(source_id.to_owned()),
434                            source_layer: feature_source_layer,
435                            source_tile: entry.source_tile(),
436                            feature_id: entry.feature.feature_id.clone(),
437                            feature_index: entry.feature.feature_index,
438                            geometry: entry.feature.geometry.clone(),
439                            properties: entry.feature.properties.clone(),
440                            state: self
441                                .feature_state
442                                .get(&FeatureStateId::new(source_id, &entry.feature.feature_id))
443                                .cloned()
444                                .unwrap_or_default(),
445                            distance_meters,
446                            from_symbol: false,
447                        });
448                    }
449                }
450                continue;
451            }
452
453            for (idx, feature) in vector_layer.features.features.iter().enumerate() {
454                if let Some(distance_meters) = geometry_hit_distance(
455                    &feature.geometry,
456                    coord,
457                    tolerance_meters,
458                ) {
459                    let feature_id = feature_id_for_feature(feature, idx);
460                    let provenance = vector_layer
461                        .feature_provenance
462                        .get(idx)
463                        .and_then(|entry| entry.as_ref());
464                    out.push(QueriedFeature {
465                        layer_id: Some(query_layer_id.to_owned()),
466                        source_id: Some(source_id.to_owned()),
467                        source_layer: provenance
468                            .and_then(|entry| entry.source_layer.clone())
469                            .or_else(|| vector_layer.query_source_layer.clone()),
470                        source_tile: provenance.and_then(|entry| entry.source_tile),
471                        feature_id: feature_id.clone(),
472                        feature_index: idx,
473                        geometry: feature.geometry.clone(),
474                        properties: feature.properties.clone(),
475                        state: self
476                            .feature_state
477                            .get(&FeatureStateId::new(source_id, &feature_id))
478                            .cloned()
479                            .unwrap_or_default(),
480                        distance_meters,
481                        from_symbol: false,
482                    });
483                }
484            }
485        }
486
487        out.sort_by(|a, b| a.distance_meters.total_cmp(&b.distance_meters));
488        out
489    }
490
491    /// Internal: query features whose geometry intersects a geographic
492    /// bounding box. This is the workhorse behind
493    /// [`query_rendered_features_in_box`](Self::query_rendered_features_in_box).
494    fn query_vector_features_in_bbox(
495        &self,
496        a: &GeoCoord,
497        b: &GeoCoord,
498        options: &QueryOptions,
499    ) -> Vec<QueriedFeature> {
500        use crate::layer::LayerKind;
501        use crate::layers::VectorLayer;
502        use crate::query::{geometry_intersects_bbox, GeoBBox};
503
504        let bbox = GeoBBox::from_geo_coords(a, b);
505
506        let filter_layers: Option<HashSet<&str>> = if options.layers.is_empty() {
507            None
508        } else {
509            Some(options.layers.iter().map(String::as_str).collect())
510        };
511        let filter_sources: Option<HashSet<&str>> = if options.sources.is_empty() {
512            None
513        } else {
514            Some(options.sources.iter().map(String::as_str).collect())
515        };
516
517        let mut out = Vec::new();
518        let mut seen_streamed = HashSet::new();
519
520        for layer in self.layers.iter() {
521            if !layer.visible() || layer.kind() != LayerKind::Vector {
522                continue;
523            }
524
525            let layer_name = layer.name();
526            let Some(vector_layer) = layer.as_any().downcast_ref::<VectorLayer>() else {
527                continue;
528            };
529
530            let query_layer_id = vector_layer
531                .query_layer_id
532                .as_deref()
533                .unwrap_or(layer_name);
534            if let Some(ref filtered_layers) = filter_layers {
535                if !filtered_layers.contains(query_layer_id) && !filtered_layers.contains(layer_name) {
536                    continue;
537                }
538            }
539
540            let source_id = vector_layer
541                .query_source_id
542                .as_deref()
543                .unwrap_or(layer_name);
544            if let Some(ref filtered_sources) = filter_sources {
545                if !filtered_sources.contains(source_id) {
546                    continue;
547                }
548            }
549
550            // Symbol layers participate only when include_symbols is set.
551            if !options.include_symbols
552                && vector_layer.style.render_mode == crate::layers::VectorRenderMode::Symbol
553            {
554                continue;
555            }
556
557            // Streamed vector payloads.
558            let streamed_payloads = self.streamed_payload_view_for(query_layer_id);
559            if !streamed_payloads.is_empty() {
560                for entry in streamed_payloads.iter() {
561                    if geometry_intersects_bbox(&entry.feature.geometry, &bbox) {
562                        let feature_source_layer = entry
563                            .source_layer(vector_layer.query_source_layer.as_deref())
564                            .map(str::to_owned);
565                        let seen_key = (
566                            source_id.to_owned(),
567                            feature_source_layer.clone(),
568                            format!("{:?}", entry.feature.geometry),
569                        );
570                        if !seen_streamed.insert(seen_key) {
571                            continue;
572                        }
573
574                        out.push(QueriedFeature {
575                            layer_id: Some(query_layer_id.to_owned()),
576                            source_id: Some(source_id.to_owned()),
577                            source_layer: feature_source_layer,
578                            source_tile: entry.source_tile(),
579                            feature_id: entry.feature.feature_id.clone(),
580                            feature_index: entry.feature.feature_index,
581                            geometry: entry.feature.geometry.clone(),
582                            properties: entry.feature.properties.clone(),
583                            state: self
584                                .feature_state
585                                .get(&FeatureStateId::new(source_id, &entry.feature.feature_id))
586                                .cloned()
587                                .unwrap_or_default(),
588                            distance_meters: 0.0,
589                            from_symbol: false,
590                        });
591                    }
592                }
593                continue;
594            }
595
596            // Local features on the runtime layer.
597            for (idx, feature) in vector_layer.features.features.iter().enumerate() {
598                if geometry_intersects_bbox(&feature.geometry, &bbox) {
599                    let feature_id = feature_id_for_feature(feature, idx);
600                    let provenance = vector_layer
601                        .feature_provenance
602                        .get(idx)
603                        .and_then(|entry| entry.as_ref());
604                    out.push(QueriedFeature {
605                        layer_id: Some(query_layer_id.to_owned()),
606                        source_id: Some(source_id.to_owned()),
607                        source_layer: provenance
608                            .and_then(|entry| entry.source_layer.clone())
609                            .or_else(|| vector_layer.query_source_layer.clone()),
610                        source_tile: provenance.and_then(|entry| entry.source_tile),
611                        feature_id: feature_id.clone(),
612                        feature_index: idx,
613                        geometry: feature.geometry.clone(),
614                        properties: feature.properties.clone(),
615                        state: self
616                            .feature_state
617                            .get(&FeatureStateId::new(source_id, &feature_id))
618                            .cloned()
619                            .unwrap_or_default(),
620                        distance_meters: 0.0,
621                        from_symbol: false,
622                    });
623                }
624            }
625        }
626
627        out
628    }
629
630    /// Look up a vector feature inside the layer stack by layer id and feature index.
631    /// Pick features at a geographic coordinate across all visible layers.
632    fn pick_features_at_geo(
633        &self,
634        coord: &GeoCoord,
635        options: &PickOptions,
636        tolerance_meters: f64,
637        hits: &mut Vec<PickHit>,
638    ) {
639        use crate::layer::LayerKind;
640        use crate::layers::{ModelLayer, VectorLayer};
641
642        let filter_layers: Option<HashSet<&str>> = if options.layers.is_empty() {
643            None
644        } else {
645            Some(options.layers.iter().map(String::as_str).collect())
646        };
647        let filter_sources: Option<HashSet<&str>> = if options.sources.is_empty() {
648            None
649        } else {
650            Some(options.sources.iter().map(String::as_str).collect())
651        };
652
653        let mut priority: usize = 0;
654
655        for layer in self.layers.iter() {
656            if !layer.visible() {
657                continue;
658            }
659
660            let layer_name = layer.name().to_owned();
661
662            if let Some(ref fl) = filter_layers {
663                if !fl.contains(layer_name.as_str()) {
664                    continue;
665                }
666            }
667
668            match layer.kind() {
669                LayerKind::Model => {
670                    if let Some(model_layer) = layer.as_any().downcast_ref::<ModelLayer>() {
671                        for inst in &model_layer.instances {
672                            if let Some(dist) = model_hit_distance(inst, coord, tolerance_meters) {
673                                hits.push(PickHit {
674                                    category: HitCategory::Model,
675                                    provenance: HitProvenance::GeometricApproximation,
676                                    layer_id: Some(layer_name.clone()),
677                                    source_id: None,
678                                    source_layer: None,
679                                    source_tile: None,
680                                    feature_id: None,
681                                    feature_index: None,
682                                    geometry: None,
683                                    properties: Default::default(),
684                                    state: Default::default(),
685                                    distance_meters: dist,
686                                    hit_coord: Some(inst.position),
687                                    layer_priority: priority as u32,
688                                    from_symbol: false,
689                                });
690                            }
691                        }
692                    }
693                }
694                LayerKind::Vector => {
695                    if let Some(vector_layer) = layer.as_any().downcast_ref::<VectorLayer>() {
696                        let source_id = vector_layer
697                            .query_source_id
698                            .as_deref()
699                            .unwrap_or(&layer_name);
700
701                        if let Some(ref fs) = filter_sources {
702                            if !fs.contains(source_id) {
703                                continue;
704                            }
705                        }
706
707                        let category = vector_render_mode_to_hit_category(
708                            &vector_layer.style.render_mode,
709                        );
710
711                        let layer_id = vector_layer
712                            .query_layer_id
713                            .as_deref()
714                            .unwrap_or(&layer_name);
715
716                        if options.include_symbols
717                            && vector_layer.style.render_mode == crate::layers::VectorRenderMode::Symbol
718                        {
719                            let symbol_payloads = self.streamed_symbol_query_payloads_for(layer_id);
720                            if !symbol_payloads.is_empty() {
721                                let mut seen_symbols = HashSet::new();
722                                for payload in symbol_payloads {
723                                    for symbol in &payload.symbols {
724                                        if let Some(dist) = symbol_hit_distance_at_geo(
725                                            symbol,
726                                            coord,
727                                            self.camera.projection(),
728                                        ) {
729                                            let seen_key = (
730                                                source_id.to_owned(),
731                                                symbol.source_layer.clone(),
732                                                symbol.feature_id.clone(),
733                                            );
734                                            if !seen_symbols.insert(seen_key) {
735                                                continue;
736                                            }
737
738                                            let state = self
739                                                .feature_state
740                                                .get(&FeatureStateId::new(source_id, &symbol.feature_id))
741                                                .cloned()
742                                                .unwrap_or_default();
743
744                                            hits.push(PickHit {
745                                                category: HitCategory::Symbol,
746                                                provenance: HitProvenance::GeometricApproximation,
747                                                layer_id: Some(layer_name.clone()),
748                                                source_id: Some(source_id.to_owned()),
749                                                source_layer: symbol
750                                                    .source_layer
751                                                    .clone()
752                                                    .or_else(|| vector_layer.query_source_layer.clone()),
753                                                source_tile: payload.tile.or(symbol.source_tile),
754                                                feature_id: Some(symbol.feature_id.clone()),
755                                                feature_index: Some(symbol.feature_index),
756                                                geometry: Some(crate::geometry::Geometry::Point(crate::geometry::Point {
757                                                    coord: symbol.anchor,
758                                                })),
759                                                properties: HashMap::new(),
760                                                state,
761                                                distance_meters: dist,
762                                                hit_coord: Some(*coord),
763                                                layer_priority: priority as u32,
764                                                from_symbol: true,
765                                            });
766                                        }
767                                    }
768                                }
769                                priority += 1;
770                                continue;
771                            }
772                        }
773
774                        if !options.include_symbols
775                            && vector_layer.style.render_mode == crate::layers::VectorRenderMode::Symbol
776                        {
777                            priority += 1;
778                            continue;
779                        }
780
781                        let streamed_payloads = self.streamed_payload_view_for(layer_id);
782                        if !streamed_payloads.is_empty() {
783                            for entry in streamed_payloads.iter() {
784                                if let Some(dist) = geometry_hit_distance(
785                                    &entry.feature.geometry,
786                                    coord,
787                                    tolerance_meters,
788                                ) {
789                                    let state = self
790                                        .feature_state
791                                        .get(&FeatureStateId::new(source_id, &entry.feature.feature_id))
792                                        .cloned()
793                                        .unwrap_or_default();
794
795                                    hits.push(PickHit {
796                                        category,
797                                        provenance: HitProvenance::GeometricApproximation,
798                                        layer_id: Some(layer_name.clone()),
799                                        source_id: Some(source_id.to_owned()),
800                                        source_layer: entry
801                                            .source_layer(vector_layer.query_source_layer.as_deref())
802                                            .map(str::to_owned),
803                                        source_tile: entry.source_tile(),
804                                        feature_id: Some(entry.feature.feature_id.clone()),
805                                        feature_index: Some(entry.feature.feature_index),
806                                        geometry: Some(entry.feature.geometry.clone()),
807                                        properties: entry.feature.properties.clone(),
808                                        state,
809                                        distance_meters: dist,
810                                        hit_coord: Some(*coord),
811                                        layer_priority: priority as u32,
812                                        from_symbol: false,
813                                    });
814                                }
815                            }
816                            priority += 1;
817                            continue;
818                        }
819
820                        for (idx, feature) in vector_layer.features.features.iter().enumerate() {
821                            if let Some(dist) = geometry_hit_distance(
822                                &feature.geometry,
823                                coord,
824                                tolerance_meters,
825                            ) {
826                                let fid = feature_id_for_feature(feature, idx);
827                                let state = self
828                                    .feature_state
829                                    .get(&FeatureStateId::new(source_id, &fid))
830                                    .cloned()
831                                    .unwrap_or_default();
832                                let provenance = vector_layer
833                                    .feature_provenance
834                                    .get(idx)
835                                    .and_then(|entry| entry.as_ref());
836
837                                hits.push(PickHit {
838                                    category,
839                                    provenance: HitProvenance::GeometricApproximation,
840                                    layer_id: Some(layer_name.clone()),
841                                    source_id: Some(source_id.to_owned()),
842                                    source_layer: provenance
843                                        .and_then(|entry| entry.source_layer.clone())
844                                        .or_else(|| vector_layer.query_source_layer.clone()),
845                                    source_tile: provenance.and_then(|entry| entry.source_tile),
846                                    feature_id: Some(fid),
847                                    feature_index: Some(idx),
848                                    geometry: Some(feature.geometry.clone()),
849                                    properties: feature.properties.clone(),
850                                    state,
851                                    distance_meters: dist,
852                                    hit_coord: Some(*coord),
853                                    layer_priority: priority as u32,
854                                    from_symbol: false,
855                                });
856                            }
857                        }
858                    }
859                }
860                LayerKind::Visualization => self.pick_visualization_layer(
861                    layer.as_any(),
862                    &layer_name,
863                    coord,
864                    tolerance_meters,
865                    priority as u32,
866                    hits,
867                ),
868                _ => {}
869            }
870
871            priority += 1;
872        }
873    }
874
875    fn pick_visualization_layer(
876        &self,
877        layer: &dyn std::any::Any,
878        layer_name: &str,
879        coord: &GeoCoord,
880        tolerance_meters: f64,
881        layer_priority: u32,
882        hits: &mut Vec<PickHit>,
883    ) {
884        if let Some(grid_layer) = layer.downcast_ref::<crate::visualization::GridScalarLayer>() {
885            if let Some((row, col)) = grid_layer.grid.cell_at_geo(coord) {
886                if let Some(value) = grid_layer.field.sample(row, col) {
887                    let cell_coord = grid_layer.grid.cell_center(row, col).unwrap_or(*coord);
888                    let mut props = HashMap::new();
889                    props.insert("value".to_owned(), PropertyValue::Number(value as f64));
890                    props.insert("row".to_owned(), PropertyValue::Number(row as f64));
891                    props.insert("col".to_owned(), PropertyValue::Number(col as f64));
892                    hits.push(PickHit {
893                        category: HitCategory::Feature,
894                        provenance: HitProvenance::GeometricApproximation,
895                        layer_id: Some(layer_name.to_owned()),
896                        source_id: None,
897                        source_layer: None,
898                        source_tile: None,
899                        feature_id: Some(format!("{row}:{col}")),
900                        feature_index: Some(row * grid_layer.grid.cols + col),
901                        geometry: None,
902                        properties: props,
903                        state: Default::default(),
904                        distance_meters: 0.0,
905                        hit_coord: Some(cell_coord),
906                        layer_priority,
907                        from_symbol: false,
908                    });
909                }
910            }
911            return;
912        }
913
914        if let Some(grid_layer) = layer.downcast_ref::<crate::visualization::GridExtrusionLayer>() {
915            if let Some((row, col)) = grid_layer.grid.cell_at_geo(coord) {
916                if let Some(value) = grid_layer.field.sample(row, col) {
917                    let cell_coord = grid_layer.grid.cell_center(row, col).unwrap_or(*coord);
918                    let mut props = HashMap::new();
919                    props.insert("value".to_owned(), PropertyValue::Number(value as f64));
920                    props.insert("row".to_owned(), PropertyValue::Number(row as f64));
921                    props.insert("col".to_owned(), PropertyValue::Number(col as f64));
922                    props.insert(
923                        "height".to_owned(),
924                        PropertyValue::Number(value as f64 * grid_layer.params.height_scale),
925                    );
926                    hits.push(PickHit {
927                        category: HitCategory::Feature,
928                        provenance: HitProvenance::GeometricApproximation,
929                        layer_id: Some(layer_name.to_owned()),
930                        source_id: None,
931                        source_layer: None,
932                        source_tile: None,
933                        feature_id: Some(format!("{row}:{col}")),
934                        feature_index: Some(row * grid_layer.grid.cols + col),
935                        geometry: None,
936                        properties: props,
937                        state: Default::default(),
938                        distance_meters: 0.0,
939                        hit_coord: Some(cell_coord),
940                        layer_priority,
941                        from_symbol: false,
942                    });
943                }
944            }
945            return;
946        }
947
948        if let Some(col_layer) = layer.downcast_ref::<crate::visualization::InstancedColumnLayer>() {
949            for (idx, column) in col_layer.columns.columns.iter().enumerate() {
950                let dist = geo_distance_meters(coord, &column.position);
951                let radius = column.width * 0.5 + tolerance_meters;
952                if dist <= radius {
953                    let mut props = HashMap::new();
954                    props.insert("pick_id".to_owned(), PropertyValue::Number(column.pick_id as f64));
955                    props.insert("height".to_owned(), PropertyValue::Number(column.height));
956                    props.insert("width".to_owned(), PropertyValue::Number(column.width));
957                    hits.push(PickHit {
958                        category: HitCategory::Feature,
959                        provenance: HitProvenance::GeometricApproximation,
960                        layer_id: Some(layer_name.to_owned()),
961                        source_id: None,
962                        source_layer: None,
963                        source_tile: None,
964                        feature_id: Some(format!("col:{}", column.pick_id)),
965                        feature_index: Some(idx),
966                        geometry: None,
967                        properties: props,
968                        state: Default::default(),
969                        distance_meters: dist,
970                        hit_coord: Some(column.position),
971                        layer_priority,
972                        from_symbol: false,
973                    });
974                }
975            }
976            return;
977        }
978
979        if let Some(point_layer) = layer.downcast_ref::<crate::visualization::PointCloudLayer>() {
980            for (idx, point) in point_layer.points.points.iter().enumerate() {
981                let dist = geo_distance_meters(coord, &point.position);
982                let radius = point.radius + tolerance_meters;
983                if dist <= radius {
984                    let mut props = HashMap::new();
985                    props.insert("pick_id".to_owned(), PropertyValue::Number(point.pick_id as f64));
986                    props.insert("radius".to_owned(), PropertyValue::Number(point.radius));
987                    props.insert("intensity".to_owned(), PropertyValue::Number(point.intensity as f64));
988                    hits.push(PickHit {
989                        category: HitCategory::Feature,
990                        provenance: HitProvenance::GeometricApproximation,
991                        layer_id: Some(layer_name.to_owned()),
992                        source_id: None,
993                        source_layer: None,
994                        source_tile: None,
995                        feature_id: Some(format!("pt:{}", point.pick_id)),
996                        feature_index: Some(idx),
997                        geometry: None,
998                        properties: props,
999                        state: Default::default(),
1000                        distance_meters: dist,
1001                        hit_coord: Some(point.position),
1002                        layer_priority,
1003                        from_symbol: false,
1004                    });
1005                }
1006            }
1007        }
1008    }
1009
1010}
1011
1012fn model_hit_distance(
1013    instance: &ModelInstance,
1014    coord: &GeoCoord,
1015    tolerance: f64,
1016) -> Option<f64> {
1017    let dlat = (coord.lat - instance.position.lat).to_radians();
1018    let dlon = (coord.lon - instance.position.lon).to_radians();
1019    let a = (dlat / 2.0).sin().powi(2)
1020        + coord.lat.to_radians().cos()
1021            * instance.position.lat.to_radians().cos()
1022            * (dlon / 2.0).sin().powi(2);
1023    let c = 2.0 * a.sqrt().asin();
1024    let dist = 6_378_137.0 * c;
1025    let radius = model_horizontal_radius(instance) + tolerance;
1026    if dist <= radius {
1027        Some(dist)
1028    } else {
1029        None
1030    }
1031}
1032
1033fn model_horizontal_radius(instance: &ModelInstance) -> f64 {
1034    instance.scale * 0.5
1035}
1036
1037fn geo_distance_meters(a: &GeoCoord, b: &GeoCoord) -> f64 {
1038    let dlat = (a.lat - b.lat).to_radians();
1039    let dlon = (a.lon - b.lon).to_radians();
1040    let hav = (dlat / 2.0).sin().powi(2)
1041        + a.lat.to_radians().cos()
1042            * b.lat.to_radians().cos()
1043            * (dlon / 2.0).sin().powi(2);
1044    let c = 2.0 * hav.sqrt().asin();
1045    6_378_137.0 * c
1046}
1047
1048fn symbol_hit_distance_at_geo(
1049    symbol: &PlacedSymbol,
1050    coord: &GeoCoord,
1051    projection: CameraProjection,
1052) -> Option<f64> {
1053    let projected = projection.project(coord);
1054    let x = projected.position.x;
1055    let y = projected.position.y;
1056    let within = x >= symbol.collision_box.min[0]
1057        && x <= symbol.collision_box.max[0]
1058        && y >= symbol.collision_box.min[1]
1059        && y <= symbol.collision_box.max[1];
1060    within.then(|| geo_distance_meters(&symbol.anchor, coord))
1061}
1062
1063fn vector_render_mode_to_hit_category(
1064    mode: &crate::layers::VectorRenderMode,
1065) -> HitCategory {
1066    use crate::layers::VectorRenderMode;
1067    match mode {
1068        VectorRenderMode::Generic => HitCategory::Feature,
1069        VectorRenderMode::Fill => HitCategory::Feature,
1070        VectorRenderMode::Line => HitCategory::Feature,
1071        VectorRenderMode::Circle => HitCategory::Feature,
1072        VectorRenderMode::Symbol => HitCategory::Symbol,
1073        VectorRenderMode::Heatmap => HitCategory::Feature,
1074        VectorRenderMode::FillExtrusion => HitCategory::Feature,
1075    }
1076}
1077
1078pub(super) fn collect_terrain_samples_from_geometry(
1079    geometry: &crate::geometry::Geometry,
1080    terrain: &TerrainManager,
1081    samples: &mut Vec<(GeoCoord, f64)>,
1082) {
1083    use crate::geometry::Geometry;
1084    match geometry {
1085        Geometry::Point(p) => {
1086            if let Some(elev) = terrain.elevation_at(&p.coord) {
1087                samples.push((p.coord, elev));
1088            }
1089        }
1090        Geometry::LineString(ls) => {
1091            for coord in &ls.coords {
1092                if let Some(elev) = terrain.elevation_at(coord) {
1093                    samples.push((*coord, elev));
1094                }
1095            }
1096        }
1097        Geometry::Polygon(poly) => {
1098            for coord in &poly.exterior {
1099                if let Some(elev) = terrain.elevation_at(coord) {
1100                    samples.push((*coord, elev));
1101                }
1102            }
1103            for hole in &poly.interiors {
1104                for coord in hole {
1105                    if let Some(elev) = terrain.elevation_at(coord) {
1106                        samples.push((*coord, elev));
1107                    }
1108                }
1109            }
1110        }
1111        Geometry::MultiPoint(mp) => {
1112            for p in &mp.points {
1113                if let Some(elev) = terrain.elevation_at(&p.coord) {
1114                    samples.push((p.coord, elev));
1115                }
1116            }
1117        }
1118        Geometry::MultiLineString(mls) => {
1119            for ls in &mls.lines {
1120                for coord in &ls.coords {
1121                    if let Some(elev) = terrain.elevation_at(coord) {
1122                        samples.push((*coord, elev));
1123                    }
1124                }
1125            }
1126        }
1127        Geometry::MultiPolygon(mpoly) => {
1128            for poly in &mpoly.polygons {
1129                for coord in &poly.exterior {
1130                    if let Some(elev) = terrain.elevation_at(coord) {
1131                        samples.push((*coord, elev));
1132                    }
1133                }
1134                for hole in &poly.interiors {
1135                    for coord in hole {
1136                        if let Some(elev) = terrain.elevation_at(coord) {
1137                            samples.push((*coord, elev));
1138                        }
1139                    }
1140                }
1141            }
1142        }
1143        Geometry::GeometryCollection(geoms) => {
1144            for g in geoms {
1145                collect_terrain_samples_from_geometry(g, terrain, samples);
1146            }
1147        }
1148    }
1149}
1150