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#[derive(Debug, Clone)]
60pub struct QueriedFeature {
61 pub layer_id: Option<String>,
63 pub source_id: Option<String>,
65 pub source_layer: Option<String>,
67 pub source_tile: Option<TileId>,
69 pub feature_id: String,
71 pub feature_index: usize,
73 pub geometry: Geometry,
75 pub properties: HashMap<String, PropertyValue>,
77 pub state: FeatureState,
79 pub distance_meters: f64,
81 pub from_symbol: bool,
83}
84
85pub fn feature_id_for_feature(feature: &Feature, feature_index: usize) -> String {
87 if let Some(value) = feature.property("id") {
88 return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
89 }
90 if let Some(value) = feature.property("feature_id") {
91 return property_value_as_feature_id(value).unwrap_or_else(|| feature_index.to_string());
92 }
93 feature_index.to_string()
94}
95
96pub fn geometry_hit_distance(
98 geometry: &Geometry,
99 coord: &GeoCoord,
100 tolerance_meters: f64,
101) -> Option<f64> {
102 match geometry {
103 Geometry::Point(point) => {
104 let dist = world_distance(coord, &point.coord);
105 (dist <= tolerance_meters).then_some(dist)
106 }
107 Geometry::LineString(line) => {
108 line_distance(&line.coords, coord).filter(|distance| *distance <= tolerance_meters)
109 }
110 Geometry::Polygon(polygon) => polygon_hit_distance(polygon, coord, tolerance_meters),
111 Geometry::MultiPoint(points) => points
112 .points
113 .iter()
114 .filter_map(|point| {
115 geometry_hit_distance(&Geometry::Point(point.clone()), coord, tolerance_meters)
116 })
117 .min_by(f64::total_cmp),
118 Geometry::MultiLineString(lines) => lines
119 .lines
120 .iter()
121 .filter_map(|line| {
122 geometry_hit_distance(&Geometry::LineString(line.clone()), coord, tolerance_meters)
123 })
124 .min_by(f64::total_cmp),
125 Geometry::MultiPolygon(polygons) => polygons
126 .polygons
127 .iter()
128 .filter_map(|polygon| {
129 geometry_hit_distance(&Geometry::Polygon(polygon.clone()), coord, tolerance_meters)
130 })
131 .min_by(f64::total_cmp),
132 Geometry::GeometryCollection(geometries) => geometries
133 .iter()
134 .filter_map(|geometry| geometry_hit_distance(geometry, coord, tolerance_meters))
135 .min_by(f64::total_cmp),
136 }
137}
138
139fn property_value_as_feature_id(value: &PropertyValue) -> Option<String> {
140 match value {
141 PropertyValue::Null => None,
142 PropertyValue::Bool(value) => Some(value.to_string()),
143 PropertyValue::Number(value) => Some(value.to_string()),
144 PropertyValue::String(value) => Some(value.clone()),
145 }
146}
147
148fn polygon_hit_distance(
149 polygon: &crate::geometry::Polygon,
150 coord: &GeoCoord,
151 tolerance_meters: f64,
152) -> Option<f64> {
153 if point_in_ring(&polygon.exterior, coord)
154 && !polygon
155 .interiors
156 .iter()
157 .any(|hole| point_in_ring(hole, coord))
158 {
159 return Some(0.0);
160 }
161
162 let mut min_distance = line_distance(&polygon.exterior, coord);
163 for hole in &polygon.interiors {
164 let hole_distance = line_distance(hole, coord);
165 min_distance = match (min_distance, hole_distance) {
166 (Some(a), Some(b)) => Some(a.min(b)),
167 (Some(a), None) => Some(a),
168 (None, Some(b)) => Some(b),
169 (None, None) => None,
170 };
171 }
172 min_distance.filter(|distance| *distance <= tolerance_meters)
173}
174
175fn line_distance(coords: &[GeoCoord], coord: &GeoCoord) -> Option<f64> {
176 if coords.len() < 2 {
177 return None;
178 }
179 let p = world_xy(coord);
180 coords
181 .windows(2)
182 .map(|segment| point_to_segment_distance(p, world_xy(&segment[0]), world_xy(&segment[1])))
183 .min_by(f64::total_cmp)
184}
185
186fn point_in_ring(ring: &[GeoCoord], coord: &GeoCoord) -> bool {
187 if ring.len() < 3 {
188 return false;
189 }
190 let p = world_xy(coord);
191 let mut inside = false;
192 let mut prev = world_xy(ring.last().expect("ring last"));
193 for current_coord in ring {
194 let current = world_xy(current_coord);
195 let intersects = ((current[1] > p[1]) != (prev[1] > p[1]))
196 && (p[0]
197 < (prev[0] - current[0]) * (p[1] - current[1])
198 / (prev[1] - current[1]).max(f64::EPSILON)
199 + current[0]);
200 if intersects {
201 inside = !inside;
202 }
203 prev = current;
204 }
205 inside
206}
207
208fn point_to_segment_distance(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
209 let ab = [b[0] - a[0], b[1] - a[1]];
210 let ap = [point[0] - a[0], point[1] - a[1]];
211 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
212 if len2 <= f64::EPSILON {
213 return ((point[0] - a[0]).powi(2) + (point[1] - a[1]).powi(2)).sqrt();
214 }
215 let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len2).clamp(0.0, 1.0);
216 let closest = [a[0] + ab[0] * t, a[1] + ab[1] * t];
217 ((point[0] - closest[0]).powi(2) + (point[1] - closest[1]).powi(2)).sqrt()
218}
219
220fn world_distance(a: &GeoCoord, b: &GeoCoord) -> f64 {
221 let a = world_xy(a);
222 let b = world_xy(b);
223 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
224}
225
226fn world_xy(coord: &GeoCoord) -> [f64; 2] {
227 let projected = WebMercator::project(coord);
228 [projected.position.x, projected.position.y]
229}
230
231#[derive(Debug, Clone, Copy)]
242pub struct GeoBBox {
243 pub min: [f64; 2],
245 pub max: [f64; 2],
247}
248
249impl GeoBBox {
250 pub fn from_geo_coords(a: &GeoCoord, b: &GeoCoord) -> Self {
255 let pa = world_xy(a);
256 let pb = world_xy(b);
257 Self {
258 min: [pa[0].min(pb[0]), pa[1].min(pb[1])],
259 max: [pa[0].max(pb[0]), pa[1].max(pb[1])],
260 }
261 }
262
263 fn contains_point(&self, p: [f64; 2]) -> bool {
265 p[0] >= self.min[0] && p[0] <= self.max[0] && p[1] >= self.min[1] && p[1] <= self.max[1]
266 }
267
268 fn overlaps(&self, other: &GeoBBox) -> bool {
270 self.min[0] <= other.max[0]
271 && self.max[0] >= other.min[0]
272 && self.min[1] <= other.max[1]
273 && self.max[1] >= other.min[1]
274 }
275}
276
277pub fn geometry_intersects_bbox(geometry: &Geometry, bbox: &GeoBBox) -> bool {
285 match geometry {
286 Geometry::Point(point) => bbox.contains_point(world_xy(&point.coord)),
287 Geometry::LineString(line) => linestring_intersects_bbox(&line.coords, bbox),
288 Geometry::Polygon(polygon) => polygon_intersects_bbox(polygon, bbox),
289 Geometry::MultiPoint(points) => points
290 .points
291 .iter()
292 .any(|p| bbox.contains_point(world_xy(&p.coord))),
293 Geometry::MultiLineString(lines) => lines
294 .lines
295 .iter()
296 .any(|line| linestring_intersects_bbox(&line.coords, bbox)),
297 Geometry::MultiPolygon(polygons) => polygons
298 .polygons
299 .iter()
300 .any(|poly| polygon_intersects_bbox(poly, bbox)),
301 Geometry::GeometryCollection(geometries) => {
302 geometries.iter().any(|g| geometry_intersects_bbox(g, bbox))
303 }
304 }
305}
306
307fn linestring_intersects_bbox(coords: &[GeoCoord], bbox: &GeoBBox) -> bool {
312 for coord in coords {
314 if bbox.contains_point(world_xy(coord)) {
315 return true;
316 }
317 }
318 for pair in coords.windows(2) {
320 let a = world_xy(&pair[0]);
321 let b = world_xy(&pair[1]);
322 if segment_intersects_bbox(a, b, bbox) {
323 return true;
324 }
325 }
326 false
327}
328
329fn polygon_intersects_bbox(polygon: &crate::geometry::Polygon, bbox: &GeoBBox) -> bool {
336 let poly_bbox = ring_bbox(&polygon.exterior);
338 if !bbox.overlaps(&poly_bbox) {
339 return false;
340 }
341 for coord in &polygon.exterior {
343 if bbox.contains_point(world_xy(coord)) {
344 return true;
345 }
346 }
347 for pair in polygon.exterior.windows(2) {
349 let a = world_xy(&pair[0]);
350 let b = world_xy(&pair[1]);
351 if segment_intersects_bbox(a, b, bbox) {
352 return true;
353 }
354 }
355 let centre_world = rustial_math::WorldCoord::new(
357 (bbox.min[0] + bbox.max[0]) * 0.5,
358 (bbox.min[1] + bbox.max[1]) * 0.5,
359 0.0,
360 );
361 let centre_geo = WebMercator::unproject(¢re_world);
362 if point_in_ring(&polygon.exterior, ¢re_geo)
363 && !polygon
364 .interiors
365 .iter()
366 .any(|hole| point_in_ring(hole, ¢re_geo))
367 {
368 return true;
369 }
370 false
371}
372
373fn ring_bbox(ring: &[GeoCoord]) -> GeoBBox {
375 let mut min = [f64::MAX, f64::MAX];
376 let mut max = [f64::MIN, f64::MIN];
377 for coord in ring {
378 let p = world_xy(coord);
379 min[0] = min[0].min(p[0]);
380 min[1] = min[1].min(p[1]);
381 max[0] = max[0].max(p[0]);
382 max[1] = max[1].max(p[1]);
383 }
384 GeoBBox { min, max }
385}
386
387fn segment_intersects_bbox(a: [f64; 2], b: [f64; 2], bbox: &GeoBBox) -> bool {
391 let dx = b[0] - a[0];
392 let dy = b[1] - a[1];
393 let mut t_min = 0.0_f64;
394 let mut t_max = 1.0_f64;
395
396 let clips = [
397 (-dx, a[0] - bbox.min[0]),
398 (dx, bbox.max[0] - a[0]),
399 (-dy, a[1] - bbox.min[1]),
400 (dy, bbox.max[1] - a[1]),
401 ];
402
403 for &(p, q) in &clips {
404 if p.abs() < f64::EPSILON {
405 if q < 0.0 {
407 return false;
408 }
409 } else {
410 let t = q / p;
411 if p < 0.0 {
412 t_min = t_min.max(t);
413 } else {
414 t_max = t_max.min(t);
415 }
416 if t_min > t_max {
417 return false;
418 }
419 }
420 }
421 true
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::geometry::{Feature, LineString, Point, Polygon};
428 use std::collections::HashMap;
429
430 #[test]
431 fn feature_id_prefers_id_property() {
432 let mut properties = HashMap::new();
433 properties.insert("id".into(), PropertyValue::String("abc".into()));
434 let feature = Feature {
435 geometry: Geometry::Point(Point {
436 coord: GeoCoord::from_lat_lon(0.0, 0.0),
437 }),
438 properties,
439 };
440 assert_eq!(feature_id_for_feature(&feature, 3), "abc");
441 }
442
443 #[test]
444 fn point_query_uses_tolerance() {
445 let geometry = Geometry::Point(Point {
446 coord: GeoCoord::from_lat_lon(0.0, 0.0),
447 });
448 assert!(geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.0, 0.0), 1.0).is_some());
449 }
450
451 #[test]
452 fn line_query_measures_distance() {
453 let geometry = Geometry::LineString(LineString {
454 coords: vec![
455 GeoCoord::from_lat_lon(0.0, 0.0),
456 GeoCoord::from_lat_lon(0.0, 0.001),
457 ],
458 });
459 assert!(
460 geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.00001, 0.0005), 32.0)
461 .is_some()
462 );
463 }
464
465 #[test]
466 fn polygon_query_hits_inside() {
467 let geometry = Geometry::Polygon(Polygon {
468 exterior: vec![
469 GeoCoord::from_lat_lon(0.0, 0.0),
470 GeoCoord::from_lat_lon(0.0, 0.01),
471 GeoCoord::from_lat_lon(0.01, 0.01),
472 GeoCoord::from_lat_lon(0.01, 0.0),
473 GeoCoord::from_lat_lon(0.0, 0.0),
474 ],
475 interiors: vec![],
476 });
477 assert_eq!(
478 geometry_hit_distance(&geometry, &GeoCoord::from_lat_lon(0.005, 0.005), 1.0),
479 Some(0.0)
480 );
481 }
482
483 fn bbox(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> GeoBBox {
489 GeoBBox::from_geo_coords(
490 &GeoCoord::from_lat_lon(lat1, lon1),
491 &GeoCoord::from_lat_lon(lat2, lon2),
492 )
493 }
494
495 #[test]
496 fn point_inside_bbox() {
497 let geom = Geometry::Point(Point {
498 coord: GeoCoord::from_lat_lon(0.005, 0.005),
499 });
500 let b = bbox(0.0, 0.0, 0.01, 0.01);
501 assert!(geometry_intersects_bbox(&geom, &b));
502 }
503
504 #[test]
505 fn point_outside_bbox() {
506 let geom = Geometry::Point(Point {
507 coord: GeoCoord::from_lat_lon(1.0, 1.0),
508 });
509 let b = bbox(0.0, 0.0, 0.01, 0.01);
510 assert!(!geometry_intersects_bbox(&geom, &b));
511 }
512
513 #[test]
514 fn line_crossing_bbox() {
515 let geom = Geometry::LineString(LineString {
518 coords: vec![
519 GeoCoord::from_lat_lon(-0.01, 0.005),
520 GeoCoord::from_lat_lon(0.02, 0.005),
521 ],
522 });
523 let b = bbox(0.0, 0.0, 0.01, 0.01);
524 assert!(geometry_intersects_bbox(&geom, &b));
525 }
526
527 #[test]
528 fn line_fully_outside_bbox() {
529 let geom = Geometry::LineString(LineString {
530 coords: vec![
531 GeoCoord::from_lat_lon(1.0, 1.0),
532 GeoCoord::from_lat_lon(1.0, 1.01),
533 ],
534 });
535 let b = bbox(0.0, 0.0, 0.01, 0.01);
536 assert!(!geometry_intersects_bbox(&geom, &b));
537 }
538
539 #[test]
540 fn polygon_overlapping_bbox() {
541 let geom = Geometry::Polygon(Polygon {
542 exterior: vec![
543 GeoCoord::from_lat_lon(0.0, 0.0),
544 GeoCoord::from_lat_lon(0.0, 0.01),
545 GeoCoord::from_lat_lon(0.01, 0.01),
546 GeoCoord::from_lat_lon(0.01, 0.0),
547 GeoCoord::from_lat_lon(0.0, 0.0),
548 ],
549 interiors: vec![],
550 });
551 let b = bbox(0.003, 0.003, 0.007, 0.007);
552 assert!(geometry_intersects_bbox(&geom, &b));
553 }
554
555 #[test]
556 fn polygon_enclosing_bbox() {
557 let geom = Geometry::Polygon(Polygon {
559 exterior: vec![
560 GeoCoord::from_lat_lon(-1.0, -1.0),
561 GeoCoord::from_lat_lon(-1.0, 1.0),
562 GeoCoord::from_lat_lon(1.0, 1.0),
563 GeoCoord::from_lat_lon(1.0, -1.0),
564 GeoCoord::from_lat_lon(-1.0, -1.0),
565 ],
566 interiors: vec![],
567 });
568 let b = bbox(0.0, 0.0, 0.01, 0.01);
569 assert!(geometry_intersects_bbox(&geom, &b));
570 }
571
572 #[test]
573 fn polygon_disjoint_from_bbox() {
574 let geom = Geometry::Polygon(Polygon {
575 exterior: vec![
576 GeoCoord::from_lat_lon(10.0, 10.0),
577 GeoCoord::from_lat_lon(10.0, 10.01),
578 GeoCoord::from_lat_lon(10.01, 10.01),
579 GeoCoord::from_lat_lon(10.01, 10.0),
580 GeoCoord::from_lat_lon(10.0, 10.0),
581 ],
582 interiors: vec![],
583 });
584 let b = bbox(0.0, 0.0, 0.01, 0.01);
585 assert!(!geometry_intersects_bbox(&geom, &b));
586 }
587}