1use crate::geometry::{Feature, Geometry, PropertyValue};
8use rustial_math::{GeoCoord, TileId, WebMercator};
9use std::collections::HashMap;
10
11pub type FeatureState = HashMap<String, PropertyValue>;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct FeatureStateId {
17 pub source_id: String,
19 pub feature_id: String,
21}
22
23impl FeatureStateId {
24 pub fn new(source_id: impl Into<String>, feature_id: impl Into<String>) -> Self {
26 Self {
27 source_id: source_id.into(),
28 feature_id: feature_id.into(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct QueryOptions {
36 pub layers: Vec<String>,
38 pub sources: Vec<String>,
40 pub tolerance_meters: f64,
42 pub include_symbols: bool,
44}
45
46impl QueryOptions {
47 pub fn new() -> Self {
49 Self {
50 layers: Vec::new(),
51 sources: Vec::new(),
52 tolerance_meters: 16.0,
53 include_symbols: true,
54 }
55 }
56
57}
58
59#[derive(Debug, Clone)]
61pub struct QueriedFeature {
62 pub layer_id: Option<String>,
64 pub source_id: Option<String>,
66 pub source_layer: Option<String>,
68 pub source_tile: Option<TileId>,
70 pub feature_id: String,
72 pub feature_index: usize,
74 pub geometry: Geometry,
76 pub properties: HashMap<String, PropertyValue>,
78 pub state: FeatureState,
80 pub distance_meters: f64,
82 pub from_symbol: bool,
84}
85
86pub fn feature_id_for_feature(feature: &Feature, feature_index: usize) -> String {
88 if let Some(value) = feature.property("id") {
89 return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
90 }
91 if let Some(value) = feature.property("feature_id") {
92 return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
93 }
94 feature_index.to_string()
95}
96
97pub fn geometry_hit_distance(
99 geometry: &Geometry,
100 coord: &GeoCoord,
101 tolerance_meters: f64,
102) -> Option<f64> {
103 match geometry {
104 Geometry::Point(point) => {
105 let dist = world_distance(coord, &point.coord);
106 (dist <= tolerance_meters).then_some(dist)
107 }
108 Geometry::LineString(line) => line_distance(&line.coords, coord)
109 .filter(|distance| *distance <= tolerance_meters),
110 Geometry::Polygon(polygon) => polygon_hit_distance(polygon, coord, tolerance_meters),
111 Geometry::MultiPoint(points) => points
112 .points
113 .iter()
114 .filter_map(|point| geometry_hit_distance(&Geometry::Point(point.clone()), coord, tolerance_meters))
115 .min_by(f64::total_cmp),
116 Geometry::MultiLineString(lines) => lines
117 .lines
118 .iter()
119 .filter_map(|line| geometry_hit_distance(&Geometry::LineString(line.clone()), coord, tolerance_meters))
120 .min_by(f64::total_cmp),
121 Geometry::MultiPolygon(polygons) => polygons
122 .polygons
123 .iter()
124 .filter_map(|polygon| geometry_hit_distance(&Geometry::Polygon(polygon.clone()), coord, tolerance_meters))
125 .min_by(f64::total_cmp),
126 Geometry::GeometryCollection(geometries) => geometries
127 .iter()
128 .filter_map(|geometry| geometry_hit_distance(geometry, coord, tolerance_meters))
129 .min_by(f64::total_cmp),
130 }
131}
132
133fn property_value_as_feature_id(value: &PropertyValue) -> Option<String> {
134 match value {
135 PropertyValue::Null => None,
136 PropertyValue::Bool(value) => Some(value.to_string()),
137 PropertyValue::Number(value) => Some(value.to_string()),
138 PropertyValue::String(value) => Some(value.clone()),
139 }
140}
141
142fn polygon_hit_distance(
143 polygon: &crate::geometry::Polygon,
144 coord: &GeoCoord,
145 tolerance_meters: f64,
146) -> Option<f64> {
147 if point_in_ring(&polygon.exterior, coord)
148 && !polygon.interiors.iter().any(|hole| point_in_ring(hole, coord))
149 {
150 return Some(0.0);
151 }
152
153 let mut min_distance = line_distance(&polygon.exterior, coord);
154 for hole in &polygon.interiors {
155 let hole_distance = line_distance(hole, coord);
156 min_distance = match (min_distance, hole_distance) {
157 (Some(a), Some(b)) => Some(a.min(b)),
158 (Some(a), None) => Some(a),
159 (None, Some(b)) => Some(b),
160 (None, None) => None,
161 };
162 }
163 min_distance.filter(|distance| *distance <= tolerance_meters)
164}
165
166fn line_distance(coords: &[GeoCoord], coord: &GeoCoord) -> Option<f64> {
167 if coords.len() < 2 {
168 return None;
169 }
170 let p = world_xy(coord);
171 coords
172 .windows(2)
173 .map(|segment| point_to_segment_distance(p, world_xy(&segment[0]), world_xy(&segment[1])))
174 .min_by(f64::total_cmp)
175}
176
177fn point_in_ring(ring: &[GeoCoord], coord: &GeoCoord) -> bool {
178 if ring.len() < 3 {
179 return false;
180 }
181 let p = world_xy(coord);
182 let mut inside = false;
183 let mut prev = world_xy(ring.last().expect("ring last"));
184 for current_coord in ring {
185 let current = world_xy(current_coord);
186 let intersects = ((current[1] > p[1]) != (prev[1] > p[1]))
187 && (p[0] < (prev[0] - current[0]) * (p[1] - current[1]) / (prev[1] - current[1]).max(f64::EPSILON) + current[0]);
188 if intersects {
189 inside = !inside;
190 }
191 prev = current;
192 }
193 inside
194}
195
196fn point_to_segment_distance(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
197 let ab = [b[0] - a[0], b[1] - a[1]];
198 let ap = [point[0] - a[0], point[1] - a[1]];
199 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
200 if len2 <= f64::EPSILON {
201 return ((point[0] - a[0]).powi(2) + (point[1] - a[1]).powi(2)).sqrt();
202 }
203 let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0);
204 let closest = [a[0] + ab[0] * t, a[1] + ab[1] * t];
205 ((point[0] - closest[0]).powi(2) + (point[1] - closest[1]).powi(2)).sqrt()
206}
207
208fn world_distance(a: &GeoCoord, b: &GeoCoord) -> f64 {
209 let a = world_xy(a);
210 let b = world_xy(b);
211 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
212}
213
214fn world_xy(coord: &GeoCoord) -> [f64; 2] {
215 let projected = WebMercator::project(coord);
216 [projected.position.x, projected.position.y]
217}
218
219#[derive(Debug, Clone, Copy)]
230pub struct GeoBBox {
231 pub min: [f64; 2],
233 pub max: [f64; 2],
235}
236
237impl GeoBBox {
238 pub fn from_geo_coords(a: &GeoCoord, b: &GeoCoord) -> Self {
243 let pa = world_xy(a);
244 let pb = world_xy(b);
245 Self {
246 min: [pa[0].min(pb[0]), pa[1].min(pb[1])],
247 max: [pa[0].max(pb[0]), pa[1].max(pb[1])],
248 }
249 }
250
251 fn contains_point(&self, p: [f64; 2]) -> bool {
253 p[0] >= self.min[0] && p[0] <= self.max[0] && p[1] >= self.min[1] && p[1] <= self.max[1]
254 }
255
256 fn overlaps(&self, other: &GeoBBox) -> bool {
258 self.min[0] <= other.max[0]
259 && self.max[0] >= other.min[0]
260 && self.min[1] <= other.max[1]
261 && self.max[1] >= other.min[1]
262 }
263}
264
265pub fn geometry_intersects_bbox(geometry: &Geometry, bbox: &GeoBBox) -> bool {
273 match geometry {
274 Geometry::Point(point) => bbox.contains_point(world_xy(&point.coord)),
275 Geometry::LineString(line) => linestring_intersects_bbox(&line.coords, bbox),
276 Geometry::Polygon(polygon) => polygon_intersects_bbox(polygon, bbox),
277 Geometry::MultiPoint(points) => points
278 .points
279 .iter()
280 .any(|p| bbox.contains_point(world_xy(&p.coord))),
281 Geometry::MultiLineString(lines) => lines
282 .lines
283 .iter()
284 .any(|line| linestring_intersects_bbox(&line.coords, bbox)),
285 Geometry::MultiPolygon(polygons) => polygons
286 .polygons
287 .iter()
288 .any(|poly| polygon_intersects_bbox(poly, bbox)),
289 Geometry::GeometryCollection(geometries) => geometries
290 .iter()
291 .any(|g| geometry_intersects_bbox(g, bbox)),
292 }
293}
294
295fn linestring_intersects_bbox(coords: &[GeoCoord], bbox: &GeoBBox) -> bool {
300 for coord in coords {
302 if bbox.contains_point(world_xy(coord)) {
303 return true;
304 }
305 }
306 for pair in coords.windows(2) {
308 let a = world_xy(&pair[0]);
309 let b = world_xy(&pair[1]);
310 if segment_intersects_bbox(a, b, bbox) {
311 return true;
312 }
313 }
314 false
315}
316
317fn polygon_intersects_bbox(polygon: &crate::geometry::Polygon, bbox: &GeoBBox) -> bool {
324 let poly_bbox = ring_bbox(&polygon.exterior);
326 if !bbox.overlaps(&poly_bbox) {
327 return false;
328 }
329 for coord in &polygon.exterior {
331 if bbox.contains_point(world_xy(coord)) {
332 return true;
333 }
334 }
335 for pair in polygon.exterior.windows(2) {
337 let a = world_xy(&pair[0]);
338 let b = world_xy(&pair[1]);
339 if segment_intersects_bbox(a, b, bbox) {
340 return true;
341 }
342 }
343 let centre_world = rustial_math::WorldCoord::new(
345 (bbox.min[0] + bbox.max[0]) * 0.5,
346 (bbox.min[1] + bbox.max[1]) * 0.5,
347 0.0,
348 );
349 let centre_geo = WebMercator::unproject(¢re_world);
350 if point_in_ring(&polygon.exterior, ¢re_geo)
351 && !polygon
352 .interiors
353 .iter()
354 .any(|hole| point_in_ring(hole, ¢re_geo))
355 {
356 return true;
357 }
358 false
359}
360
361fn ring_bbox(ring: &[GeoCoord]) -> GeoBBox {
363 let mut min = [f64::MAX, f64::MAX];
364 let mut max = [f64::MIN, f64::MIN];
365 for coord in ring {
366 let p = world_xy(coord);
367 min[0] = min[0].min(p[0]);
368 min[1] = min[1].min(p[1]);
369 max[0] = max[0].max(p[0]);
370 max[1] = max[1].max(p[1]);
371 }
372 GeoBBox { min, max }
373}
374
375fn segment_intersects_bbox(a: [f64; 2], b: [f64; 2], bbox: &GeoBBox) -> bool {
379 let dx = b[0] - a[0];
380 let dy = b[1] - a[1];
381 let mut t_min = 0.0_f64;
382 let mut t_max = 1.0_f64;
383
384 let clips = [
385 (-dx, a[0] - bbox.min[0]),
386 (dx, bbox.max[0] - a[0]),
387 (-dy, a[1] - bbox.min[1]),
388 (dy, bbox.max[1] - a[1]),
389 ];
390
391 for &(p, q) in &clips {
392 if p.abs() < f64::EPSILON {
393 if q < 0.0 {
395 return false;
396 }
397 } else {
398 let t = q / p;
399 if p < 0.0 {
400 t_min = t_min.max(t);
401 } else {
402 t_max = t_max.min(t);
403 }
404 if t_min > t_max {
405 return false;
406 }
407 }
408 }
409 true
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::geometry::{Feature, LineString, Point, Polygon};
416 use std::collections::HashMap;
417
418 #[test]
419 fn feature_id_prefers_id_property() {
420 let mut properties = HashMap::new();
421 properties.insert("id".into(), PropertyValue::String("abc".into()));
422 let feature = Feature {
423 geometry: Geometry::Point(Point {
424 coord: GeoCoord::from_lat_lon(0.0, 0.0),
425 }),
426 properties,
427 };
428 assert_eq!(feature_id_for_feature(&feature, 3), "abc");
429 }
430
431 #[test]
432 fn point_query_uses_tolerance() {
433 let geometry = Geometry::Point(Point {
434 coord: GeoCoord::from_lat_lon(0.0, 0.0),
435 });
436 assert!(geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.0, 0.0), 1.0).is_some());
437 }
438
439 #[test]
440 fn line_query_measures_distance() {
441 let geometry = Geometry::LineString(LineString {
442 coords: vec![GeoCoord::from_lat_lon(0.0, 0.0), GeoCoord::from_lat_lon(0.0, 0.001)],
443 });
444 assert!(geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.00001, 0.0005), 32.0).is_some());
445 }
446
447 #[test]
448 fn polygon_query_hits_inside() {
449 let geometry = Geometry::Polygon(Polygon {
450 exterior: vec![
451 GeoCoord::from_lat_lon(0.0, 0.0),
452 GeoCoord::from_lat_lon(0.0, 0.01),
453 GeoCoord::from_lat_lon(0.01, 0.01),
454 GeoCoord::from_lat_lon(0.01, 0.0),
455 GeoCoord::from_lat_lon(0.0, 0.0),
456 ],
457 interiors: vec![],
458 });
459 assert_eq!(geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.005, 0.005), 1.0), Some(0.0));
460 }
461
462 fn bbox(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> GeoBBox {
468 GeoBBox::from_geo_coords(
469 &GeoCoord::from_lat_lon(lat1, lon1),
470 &GeoCoord::from_lat_lon(lat2, lon2),
471 )
472 }
473
474 #[test]
475 fn point_inside_bbox() {
476 let geom = Geometry::Point(Point {
477 coord: GeoCoord::from_lat_lon(0.005, 0.005),
478 });
479 let b = bbox(0.0, 0.0, 0.01, 0.01);
480 assert!(geometry_intersects_bbox(&geom, &b));
481 }
482
483 #[test]
484 fn point_outside_bbox() {
485 let geom = Geometry::Point(Point {
486 coord: GeoCoord::from_lat_lon(1.0, 1.0),
487 });
488 let b = bbox(0.0, 0.0, 0.01, 0.01);
489 assert!(!geometry_intersects_bbox(&geom, &b));
490 }
491
492 #[test]
493 fn line_crossing_bbox() {
494 let geom = Geometry::LineString(LineString {
497 coords: vec![
498 GeoCoord::from_lat_lon(-0.01, 0.005),
499 GeoCoord::from_lat_lon(0.02, 0.005),
500 ],
501 });
502 let b = bbox(0.0, 0.0, 0.01, 0.01);
503 assert!(geometry_intersects_bbox(&geom, &b));
504 }
505
506 #[test]
507 fn line_fully_outside_bbox() {
508 let geom = Geometry::LineString(LineString {
509 coords: vec![
510 GeoCoord::from_lat_lon(1.0, 1.0),
511 GeoCoord::from_lat_lon(1.0, 1.01),
512 ],
513 });
514 let b = bbox(0.0, 0.0, 0.01, 0.01);
515 assert!(!geometry_intersects_bbox(&geom, &b));
516 }
517
518 #[test]
519 fn polygon_overlapping_bbox() {
520 let geom = Geometry::Polygon(Polygon {
521 exterior: vec![
522 GeoCoord::from_lat_lon(0.0, 0.0),
523 GeoCoord::from_lat_lon(0.0, 0.01),
524 GeoCoord::from_lat_lon(0.01, 0.01),
525 GeoCoord::from_lat_lon(0.01, 0.0),
526 GeoCoord::from_lat_lon(0.0, 0.0),
527 ],
528 interiors: vec![],
529 });
530 let b = bbox(0.003, 0.003, 0.007, 0.007);
531 assert!(geometry_intersects_bbox(&geom, &b));
532 }
533
534 #[test]
535 fn polygon_enclosing_bbox() {
536 let geom = Geometry::Polygon(Polygon {
538 exterior: vec![
539 GeoCoord::from_lat_lon(-1.0, -1.0),
540 GeoCoord::from_lat_lon(-1.0, 1.0),
541 GeoCoord::from_lat_lon(1.0, 1.0),
542 GeoCoord::from_lat_lon(1.0, -1.0),
543 GeoCoord::from_lat_lon(-1.0, -1.0),
544 ],
545 interiors: vec![],
546 });
547 let b = bbox(0.0, 0.0, 0.01, 0.01);
548 assert!(geometry_intersects_bbox(&geom, &b));
549 }
550
551 #[test]
552 fn polygon_disjoint_from_bbox() {
553 let geom = Geometry::Polygon(Polygon {
554 exterior: vec![
555 GeoCoord::from_lat_lon(10.0, 10.0),
556 GeoCoord::from_lat_lon(10.0, 10.01),
557 GeoCoord::from_lat_lon(10.01, 10.01),
558 GeoCoord::from_lat_lon(10.01, 10.0),
559 GeoCoord::from_lat_lon(10.0, 10.0),
560 ],
561 interiors: vec![],
562 });
563 let b = bbox(0.0, 0.0, 0.01, 0.01);
564 assert!(!geometry_intersects_bbox(&geom, &b));
565 }
566}