use crate::camera_projection::CameraProjection;
use crate::geometry::PropertyValue;
use crate::query::FeatureState;
#[cfg(test)]
use crate::query::QueryOptions;
use rustial_math::{GeoCoord, TileId};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PickableLayerKind {
Fill,
Line,
Circle,
Heatmap,
FillExtrusion,
Symbol,
Model,
}
impl PickableLayerKind {
#[inline]
pub fn is_queryable(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NonPickableLayerKind {
Raster,
Background,
Hillshade,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HitProvenance {
GeometricApproximation,
TerrainSurface,
RendererExact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HitCategory {
Terrain,
Feature,
Symbol,
Model,
}
#[derive(Debug, Clone)]
pub enum PickQuery {
Screen {
x: f64,
y: f64,
},
Geo {
coord: GeoCoord,
},
Ray {
origin: glam::DVec3,
direction: glam::DVec3,
},
}
impl PickQuery {
pub fn screen(x: f64, y: f64) -> Self {
Self::Screen { x, y }
}
pub fn geo(coord: GeoCoord) -> Self {
Self::Geo { coord }
}
pub fn ray(origin: glam::DVec3, direction: glam::DVec3) -> Self {
Self::Ray { origin, direction }
}
}
#[derive(Debug, Clone)]
pub struct PickOptions {
pub layers: Vec<String>,
pub sources: Vec<String>,
pub tolerance_meters: f64,
pub include_symbols: bool,
pub include_models: bool,
pub include_terrain_surface: bool,
pub limit: usize,
}
impl Default for PickOptions {
fn default() -> Self {
Self {
layers: Vec::new(),
sources: Vec::new(),
tolerance_meters: 16.0,
include_symbols: true,
include_models: true,
include_terrain_surface: false,
limit: 0,
}
}
}
impl PickOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_terrain_surface(mut self) -> Self {
self.include_terrain_surface = true;
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
pub fn with_layers(mut self, layers: Vec<String>) -> Self {
self.layers = layers;
self
}
pub fn with_sources(mut self, sources: Vec<String>) -> Self {
self.sources = sources;
self
}
#[cfg(test)]
pub(crate) fn to_query_options(&self) -> QueryOptions {
QueryOptions {
layers: self.layers.clone(),
sources: self.sources.clone(),
tolerance_meters: self.tolerance_meters,
include_symbols: self.include_symbols,
}
}
}
#[derive(Debug, Clone)]
pub struct PickHit {
pub category: HitCategory,
pub provenance: HitProvenance,
pub layer_id: Option<String>,
pub source_id: Option<String>,
pub source_layer: Option<String>,
pub source_tile: Option<TileId>,
pub feature_id: Option<String>,
pub feature_index: Option<usize>,
pub geometry: Option<crate::geometry::Geometry>,
pub properties: HashMap<String, PropertyValue>,
pub state: FeatureState,
pub distance_meters: f64,
pub hit_coord: Option<GeoCoord>,
pub layer_priority: u32,
pub from_symbol: bool,
}
impl PickHit {
pub fn terrain_surface(coord: GeoCoord, elevation: Option<f64>) -> Self {
Self {
category: HitCategory::Terrain,
provenance: HitProvenance::TerrainSurface,
layer_id: None,
source_id: None,
source_layer: None,
source_tile: None,
feature_id: None,
feature_index: None,
geometry: Some(crate::geometry::Geometry::Point(crate::geometry::Point {
coord: GeoCoord::new(coord.lat, coord.lon, elevation.unwrap_or(coord.alt)),
})),
properties: HashMap::new(),
state: HashMap::new(),
distance_meters: 0.0,
hit_coord: Some(coord),
layer_priority: u32::MAX,
from_symbol: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PickResult {
pub hits: Vec<PickHit>,
pub query_coord: Option<GeoCoord>,
pub projection: Option<CameraProjection>,
}
impl PickResult {
#[inline]
pub fn is_empty(&self) -> bool {
self.hits.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.hits.len()
}
pub fn iter(&self) -> impl Iterator<Item = &PickHit> {
self.hits.iter()
}
pub fn first(&self) -> Option<&PickHit> {
self.hits.first()
}
pub fn by_category(&self, category: HitCategory) -> Vec<&PickHit> {
self.hits
.iter()
.filter(|h| h.category == category)
.collect()
}
pub fn terrain_hits(&self) -> Vec<&PickHit> {
self.by_category(HitCategory::Terrain)
}
pub fn feature_hits(&self) -> Vec<&PickHit> {
self.by_category(HitCategory::Feature)
}
pub fn symbol_hits(&self) -> Vec<&PickHit> {
self.by_category(HitCategory::Symbol)
}
pub fn model_hits(&self) -> Vec<&PickHit> {
self.by_category(HitCategory::Model)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pickable_layer_kinds_are_all_queryable() {
assert!(PickableLayerKind::Fill.is_queryable());
assert!(PickableLayerKind::Line.is_queryable());
assert!(PickableLayerKind::Circle.is_queryable());
assert!(PickableLayerKind::Heatmap.is_queryable());
assert!(PickableLayerKind::FillExtrusion.is_queryable());
assert!(PickableLayerKind::Symbol.is_queryable());
assert!(PickableLayerKind::Model.is_queryable());
}
#[test]
fn pick_query_constructors() {
let screen = PickQuery::screen(400.0, 300.0);
assert!(matches!(screen, PickQuery::Screen { x: 400.0, y: 300.0 }));
let geo = PickQuery::geo(GeoCoord::from_lat_lon(51.1, 17.0));
assert!(matches!(geo, PickQuery::Geo { .. }));
let ray = PickQuery::ray(glam::DVec3::ZERO, -glam::DVec3::Z);
assert!(matches!(ray, PickQuery::Ray { .. }));
}
#[test]
fn pick_options_defaults() {
let opts = PickOptions::default();
assert!(opts.layers.is_empty());
assert!(opts.sources.is_empty());
assert!(opts.include_symbols);
assert!(opts.include_models);
assert!(!opts.include_terrain_surface);
assert_eq!(opts.limit, 0);
}
#[test]
fn pick_options_builder() {
let opts = PickOptions::new()
.with_terrain_surface()
.with_limit(5)
.with_layers(vec!["layer-a".into()]);
assert!(opts.include_terrain_surface);
assert_eq!(opts.limit, 5);
assert_eq!(opts.layers, vec!["layer-a"]);
}
#[test]
fn pick_result_filtering() {
let mut result = PickResult::default();
result.hits.push(PickHit::terrain_surface(
GeoCoord::from_lat_lon(10.0, 20.0),
Some(100.0),
));
result.hits.push(PickHit {
category: HitCategory::Feature,
provenance: HitProvenance::GeometricApproximation,
layer_id: Some("fills".into()),
source_id: None,
source_layer: None,
source_tile: None,
feature_id: Some("42".into()),
feature_index: Some(42),
geometry: None,
properties: HashMap::new(),
state: HashMap::new(),
distance_meters: 5.0,
hit_coord: None,
layer_priority: 0,
from_symbol: false,
});
assert_eq!(result.len(), 2);
assert_eq!(result.terrain_hits().len(), 1);
assert_eq!(result.feature_hits().len(), 1);
assert_eq!(result.symbol_hits().len(), 0);
assert_eq!(result.model_hits().len(), 0);
}
#[test]
fn terrain_surface_hit_has_correct_metadata() {
let hit = PickHit::terrain_surface(GeoCoord::new(10.0, 20.0, 50.0), Some(100.0));
assert_eq!(hit.category, HitCategory::Terrain);
assert_eq!(hit.provenance, HitProvenance::TerrainSurface);
assert!(hit.layer_id.is_none());
assert!(hit.feature_id.is_none());
assert_eq!(hit.layer_priority, u32::MAX);
if let Some(GeoCoord { lat, lon, alt: _ }) = hit.hit_coord {
assert!((lat - 10.0).abs() < 1e-9);
assert!((lon - 20.0).abs() < 1e-9);
}
}
#[test]
fn hit_provenance_distinguishes_methods() {
assert_ne!(
HitProvenance::GeometricApproximation,
HitProvenance::TerrainSurface
);
assert_ne!(HitProvenance::TerrainSurface, HitProvenance::RendererExact);
}
#[test]
fn to_query_options_preserves_fields() {
let opts = PickOptions {
layers: vec!["a".into()],
sources: vec!["b".into()],
tolerance_meters: 32.0,
include_symbols: false,
..Default::default()
};
let qo = opts.to_query_options();
assert_eq!(qo.layers, vec!["a"]);
assert_eq!(qo.sources, vec!["b"]);
assert!((qo.tolerance_meters - 32.0).abs() < 1e-9);
assert!(!qo.include_symbols);
}
}