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