use crate::geometry::{Feature, Geometry, PropertyValue};
use rustial_math::{GeoCoord, TileId, WebMercator};
use std::collections::HashMap;
pub type FeatureState = HashMap<String, PropertyValue>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FeatureStateId {
pub source_id: String,
pub feature_id: String,
}
impl FeatureStateId {
pub fn new(source_id: impl Into<String>, feature_id: impl Into<String>) -> Self {
Self {
source_id: source_id.into(),
feature_id: feature_id.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct QueryOptions {
pub layers: Vec<String>,
pub sources: Vec<String>,
pub tolerance_meters: f64,
pub include_symbols: bool,
}
impl QueryOptions {
pub fn new() -> Self {
Self {
layers: Vec::new(),
sources: Vec::new(),
tolerance_meters: 16.0,
include_symbols: true,
}
}
}
#[derive(Debug, Clone)]
pub struct QueriedFeature {
pub layer_id: Option<String>,
pub source_id: Option<String>,
pub source_layer: Option<String>,
pub source_tile: Option<TileId>,
pub feature_id: String,
pub feature_index: usize,
pub geometry: Geometry,
pub properties: HashMap<String, PropertyValue>,
pub state: FeatureState,
pub distance_meters: f64,
pub from_symbol: bool,
}
pub fn feature_id_for_feature(feature: &Feature, feature_index: usize) -> String {
if let Some(value) = feature.property("id") {
return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
}
if let Some(value) = feature.property("feature_id") {
return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
}
feature_index.to_string()
}
pub fn geometry_hit_distance(
geometry: &Geometry,
coord: &GeoCoord,
tolerance_meters: f64,
) -> Option<f64> {
match geometry {
Geometry::Point(point) => {
let dist = world_distance(coord, &point.coord);
(dist <= tolerance_meters).then_some(dist)
}
Geometry::LineString(line) => {
line_distance(&line.coords, coord).filter(|distance| *distance <= tolerance_meters)
}
Geometry::Polygon(polygon) => polygon_hit_distance(polygon, coord, tolerance_meters),
Geometry::MultiPoint(points) => points
.points
.iter()
.filter_map(|point| {
geometry_hit_distance(&Geometry::Point(point.clone()), coord, tolerance_meters)
})
.min_by(f64::total_cmp),
Geometry::MultiLineString(lines) => lines
.lines
.iter()
.filter_map(|line| {
geometry_hit_distance(&Geometry::LineString(line.clone()), coord, tolerance_meters)
})
.min_by(f64::total_cmp),
Geometry::MultiPolygon(polygons) => polygons
.polygons
.iter()
.filter_map(|polygon| {
geometry_hit_distance(&Geometry::Polygon(polygon.clone()), coord, tolerance_meters)
})
.min_by(f64::total_cmp),
Geometry::GeometryCollection(geometries) => geometries
.iter()
.filter_map(|geometry| geometry_hit_distance(geometry, coord, tolerance_meters))
.min_by(f64::total_cmp),
}
}
fn property_value_as_feature_id(value: &PropertyValue) -> Option<String> {
match value {
PropertyValue::Null => None,
PropertyValue::Bool(value) => Some(value.to_string()),
PropertyValue::Number(value) => Some(value.to_string()),
PropertyValue::String(value) => Some(value.clone()),
}
}
fn polygon_hit_distance(
polygon: &crate::geometry::Polygon,
coord: &GeoCoord,
tolerance_meters: f64,
) -> Option<f64> {
if point_in_ring(&polygon.exterior, coord)
&& !polygon
.interiors
.iter()
.any(|hole| point_in_ring(hole, coord))
{
return Some(0.0);
}
let mut min_distance = line_distance(&polygon.exterior, coord);
for hole in &polygon.interiors {
let hole_distance = line_distance(hole, coord);
min_distance = match (min_distance, hole_distance) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
};
}
min_distance.filter(|distance| *distance <= tolerance_meters)
}
fn line_distance(coords: &[GeoCoord], coord: &GeoCoord) -> Option<f64> {
if coords.len() < 2 {
return None;
}
let p = world_xy(coord);
coords
.windows(2)
.map(|segment| point_to_segment_distance(p, world_xy(&segment[0]), world_xy(&segment[1])))
.min_by(f64::total_cmp)
}
fn point_in_ring(ring: &[GeoCoord], coord: &GeoCoord) -> bool {
if ring.len() < 3 {
return false;
}
let p = world_xy(coord);
let mut inside = false;
let mut prev = world_xy(ring.last().expect("ring last"));
for current_coord in ring {
let current = world_xy(current_coord);
let intersects = ((current[1] > p[1]) != (prev[1] > p[1]))
&& (p[0]
< (prev[0] - current[0]) * (p[1] - current[1])
/ (prev[1] - current[1]).max(f64::EPSILON)
+ current[0]);
if intersects {
inside = !inside;
}
prev = current;
}
inside
}
fn point_to_segment_distance(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let ab = [b[0] - a[0], b[1] - a[1]];
let ap = [point[0] - a[0], point[1] - a[1]];
let len2 = ab[0] * ab[0] + ab[1] * ab[1];
if len2 <= f64::EPSILON {
return ((point[0] - a[0]).powi(2) + (point[1] - a[1]).powi(2)).sqrt();
}
let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0);
let closest = [a[0] + ab[0] * t, a[1] + ab[1] * t];
((point[0] - closest[0]).powi(2) + (point[1] - closest[1]).powi(2)).sqrt()
}
fn world_distance(a: &GeoCoord, b: &GeoCoord) -> f64 {
let a = world_xy(a);
let b = world_xy(b);
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
}
fn world_xy(coord: &GeoCoord) -> [f64; 2] {
let projected = WebMercator::project(coord);
[projected.position.x, projected.position.y]
}
#[derive(Debug, Clone, Copy)]
pub struct GeoBBox {
pub min: [f64; 2],
pub max: [f64; 2],
}
impl GeoBBox {
pub fn from_geo_coords(a: &GeoCoord, b: &GeoCoord) -> Self {
let pa = world_xy(a);
let pb = world_xy(b);
Self {
min: [pa[0].min(pb[0]), pa[1].min(pb[1])],
max: [pa[0].max(pb[0]), pa[1].max(pb[1])],
}
}
fn contains_point(&self, p: [f64; 2]) -> bool {
p[0] >= self.min[0] && p[0] <= self.max[0] && p[1] >= self.min[1] && p[1] <= self.max[1]
}
fn overlaps(&self, other: &GeoBBox) -> bool {
self.min[0] <= other.max[0]
&& self.max[0] >= other.min[0]
&& self.min[1] <= other.max[1]
&& self.max[1] >= other.min[1]
}
}
pub fn geometry_intersects_bbox(geometry: &Geometry, bbox: &GeoBBox) -> bool {
match geometry {
Geometry::Point(point) => bbox.contains_point(world_xy(&point.coord)),
Geometry::LineString(line) => linestring_intersects_bbox(&line.coords, bbox),
Geometry::Polygon(polygon) => polygon_intersects_bbox(polygon, bbox),
Geometry::MultiPoint(points) => points
.points
.iter()
.any(|p| bbox.contains_point(world_xy(&p.coord))),
Geometry::MultiLineString(lines) => lines
.lines
.iter()
.any(|line| linestring_intersects_bbox(&line.coords, bbox)),
Geometry::MultiPolygon(polygons) => polygons
.polygons
.iter()
.any(|poly| polygon_intersects_bbox(poly, bbox)),
Geometry::GeometryCollection(geometries) => {
geometries.iter().any(|g| geometry_intersects_bbox(g, bbox))
}
}
}
fn linestring_intersects_bbox(coords: &[GeoCoord], bbox: &GeoBBox) -> bool {
for coord in coords {
if bbox.contains_point(world_xy(coord)) {
return true;
}
}
for pair in coords.windows(2) {
let a = world_xy(&pair[0]);
let b = world_xy(&pair[1]);
if segment_intersects_bbox(a, b, bbox) {
return true;
}
}
false
}
fn polygon_intersects_bbox(polygon: &crate::geometry::Polygon, bbox: &GeoBBox) -> bool {
let poly_bbox = ring_bbox(&polygon.exterior);
if !bbox.overlaps(&poly_bbox) {
return false;
}
for coord in &polygon.exterior {
if bbox.contains_point(world_xy(coord)) {
return true;
}
}
for pair in polygon.exterior.windows(2) {
let a = world_xy(&pair[0]);
let b = world_xy(&pair[1]);
if segment_intersects_bbox(a, b, bbox) {
return true;
}
}
let centre_world = rustial_math::WorldCoord::new(
(bbox.min[0] + bbox.max[0]) * 0.5,
(bbox.min[1] + bbox.max[1]) * 0.5,
0.0,
);
let centre_geo = WebMercator::unproject(¢re_world);
if point_in_ring(&polygon.exterior, ¢re_geo)
&& !polygon
.interiors
.iter()
.any(|hole| point_in_ring(hole, ¢re_geo))
{
return true;
}
false
}
fn ring_bbox(ring: &[GeoCoord]) -> GeoBBox {
let mut min = [f64::MAX, f64::MAX];
let mut max = [f64::MIN, f64::MIN];
for coord in ring {
let p = world_xy(coord);
min[0] = min[0].min(p[0]);
min[1] = min[1].min(p[1]);
max[0] = max[0].max(p[0]);
max[1] = max[1].max(p[1]);
}
GeoBBox { min, max }
}
fn segment_intersects_bbox(a: [f64; 2], b: [f64; 2], bbox: &GeoBBox) -> bool {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let mut t_min = 0.0_f64;
let mut t_max = 1.0_f64;
let clips = [
(-dx, a[0] - bbox.min[0]),
(dx, bbox.max[0] - a[0]),
(-dy, a[1] - bbox.min[1]),
(dy, bbox.max[1] - a[1]),
];
for &(p, q) in &clips {
if p.abs() < f64::EPSILON {
if q < 0.0 {
return false;
}
} else {
let t = q / p;
if p < 0.0 {
t_min = t_min.max(t);
} else {
t_max = t_max.min(t);
}
if t_min > t_max {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Feature, LineString, Point, Polygon};
use std::collections::HashMap;
#[test]
fn feature_id_prefers_id_property() {
let mut properties = HashMap::new();
properties.insert("id".into(), PropertyValue::String("abc".into()));
let feature = Feature {
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(0.0, 0.0),
}),
properties,
};
assert_eq!(feature_id_for_feature(&feature, 3), "abc");
}
#[test]
fn point_query_uses_tolerance() {
let geometry = Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(0.0, 0.0),
});
assert!(geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.0, 0.0), 1.0).is_some());
}
#[test]
fn line_query_measures_distance() {
let geometry = Geometry::LineString(LineString {
coords: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.001),
],
});
assert!(
geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.00001, 0.0005), 32.0)
.is_some()
);
}
#[test]
fn polygon_query_hits_inside() {
let geometry = Geometry::Polygon(Polygon {
exterior: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.01),
GeoCoord::from_lat_lon(0.01, 0.01),
GeoCoord::from_lat_lon(0.01, 0.0),
GeoCoord::from_lat_lon(0.0, 0.0),
],
interiors: vec![],
});
assert_eq!(
geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.005, 0.005), 1.0),
Some(0.0)
);
}
fn bbox(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> GeoBBox {
GeoBBox::from_geo_coords(
&GeoCoord::from_lat_lon(lat1, lon1),
&GeoCoord::from_lat_lon(lat2, lon2),
)
}
#[test]
fn point_inside_bbox() {
let geom = Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(0.005, 0.005),
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(geometry_intersects_bbox(&geom, &b));
}
#[test]
fn point_outside_bbox() {
let geom = Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(1.0, 1.0),
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(!geometry_intersects_bbox(&geom, &b));
}
#[test]
fn line_crossing_bbox() {
let geom = Geometry::LineString(LineString {
coords: vec![
GeoCoord::from_lat_lon(-0.01, 0.005),
GeoCoord::from_lat_lon(0.02, 0.005),
],
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(geometry_intersects_bbox(&geom, &b));
}
#[test]
fn line_fully_outside_bbox() {
let geom = Geometry::LineString(LineString {
coords: vec![
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.01),
],
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(!geometry_intersects_bbox(&geom, &b));
}
#[test]
fn polygon_overlapping_bbox() {
let geom = Geometry::Polygon(Polygon {
exterior: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.01),
GeoCoord::from_lat_lon(0.01, 0.01),
GeoCoord::from_lat_lon(0.01, 0.0),
GeoCoord::from_lat_lon(0.0, 0.0),
],
interiors: vec![],
});
let b = bbox(0.003, 0.003, 0.007, 0.007);
assert!(geometry_intersects_bbox(&geom, &b));
}
#[test]
fn polygon_enclosing_bbox() {
let geom = Geometry::Polygon(Polygon {
exterior: vec![
GeoCoord::from_lat_lon(-1.0, -1.0),
GeoCoord::from_lat_lon(-1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, -1.0),
GeoCoord::from_lat_lon(-1.0, -1.0),
],
interiors: vec![],
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(geometry_intersects_bbox(&geom, &b));
}
#[test]
fn polygon_disjoint_from_bbox() {
let geom = Geometry::Polygon(Polygon {
exterior: vec![
GeoCoord::from_lat_lon(10.0, 10.0),
GeoCoord::from_lat_lon(10.0, 10.01),
GeoCoord::from_lat_lon(10.01, 10.01),
GeoCoord::from_lat_lon(10.01, 10.0),
GeoCoord::from_lat_lon(10.0, 10.0),
],
interiors: vec![],
});
let b = bbox(0.0, 0.0, 0.01, 0.01);
assert!(!geometry_intersects_bbox(&geom, &b));
}
}