Skip to main content

reinhardt_db/orm/
gis.rs

1/// GIS (Geographic Information System) Support
2/// Spatial data types and operations
3use serde::{Deserialize, Serialize};
4
5// ============= UTILITY FUNCTIONS =============
6
7/// Calculate distance between two points using the Haversine formula
8/// This is accurate for geographic coordinates (latitude/longitude)
9fn haversine_distance(p1: &Point, p2: &Point) -> f64 {
10	const EARTH_RADIUS_KM: f64 = 6371.0;
11
12	let lat1 = p1.y.to_radians();
13	let lat2 = p2.y.to_radians();
14	let delta_lat = (p2.y - p1.y).to_radians();
15	let delta_lon = (p2.x - p1.x).to_radians();
16
17	let a =
18		(delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
19	let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
20
21	EARTH_RADIUS_KM * c * 1000.0 // Return in meters
22}
23
24// ============= SPATIAL DATA TYPES =============
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27/// Represents a point.
28pub struct Point {
29	/// The x.
30	pub x: f64,
31	/// The y.
32	pub y: f64,
33	/// The srid.
34	pub srid: i32, // Spatial Reference System Identifier
35}
36
37impl Point {
38	/// Create a new geographic Point with WGS 84 coordinate system (SRID 4326)
39	///
40	/// # Examples
41	///
42	/// ```
43	/// use reinhardt_db::orm::gis::Point;
44	///
45	/// let tokyo = Point::new(139.6917, 35.6895); // Longitude, Latitude
46	/// assert_eq!(tokyo.x, 139.6917);
47	/// assert_eq!(tokyo.y, 35.6895);
48	/// assert_eq!(tokyo.srid, 4326); // WGS 84 (GPS coordinates)
49	/// ```
50	pub fn new(x: f64, y: f64) -> Self {
51		Self { x, y, srid: 4326 } // WGS 84
52	}
53	/// Documentation for `with_srid`
54	pub fn with_srid(x: f64, y: f64, srid: i32) -> Self {
55		Self { x, y, srid }
56	}
57	/// Calculate distance to another point
58	/// Automatically selects appropriate method based on SRID:
59	/// - SRID 4326 (WGS84): Uses Haversine formula for geographic distance in meters
60	/// - Other SRIDs: Uses Euclidean distance for planar coordinates
61	///
62	pub fn distance_to(&self, other: &Point) -> f64 {
63		// If both points use WGS84 (SRID 4326), use Haversine for accurate geographic distance
64		if self.srid == 4326 && other.srid == 4326 {
65			haversine_distance(self, other)
66		} else if self.srid != other.srid {
67			// Points have different SRIDs - this should be handled by transformation first
68			eprintln!(
69				"Warning: Computing distance between points with different SRIDs ({} and {}). \
70                 Consider transforming to a common SRID first for accurate results.",
71				self.srid, other.srid
72			);
73			// Fall back to Euclidean distance
74			((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
75		} else {
76			// Same SRID, not WGS84 - use planar Euclidean distance
77			((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
78		}
79	}
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83/// Represents a line string.
84pub struct LineString {
85	/// The points.
86	pub points: Vec<Point>,
87	/// The srid.
88	pub srid: i32,
89}
90
91impl LineString {
92	/// Calculate length of the linestring using Euclidean distance
93	/// Note: This calculates planar distance. For geographic coordinates,
94	/// consider using great-circle or geodesic distance calculations.
95	///
96	pub fn length(&self) -> f64 {
97		self.points
98			.array_windows::<2>()
99			.map(|[p1, p2]| {
100				let dx = p2.x - p1.x;
101				let dy = p2.y - p1.y;
102				(dx * dx + dy * dy).sqrt()
103			})
104			.sum()
105	}
106	/// Calculate length considering the SRID (geographic distance if using WGS84)
107	///
108	pub fn geodesic_length(&self) -> f64 {
109		if self.srid == 4326 {
110			// WGS84 - use Haversine formula for better accuracy
111			self.points
112				.array_windows::<2>()
113				.map(|[p1, p2]| haversine_distance(p1, p2))
114				.sum()
115		} else {
116			// For other SRIDs, fall back to planar distance
117			self.length()
118		}
119	}
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123/// Represents a polygon.
124pub struct Polygon {
125	/// The exterior.
126	pub exterior: Vec<Point>,
127	/// The interiors.
128	pub interiors: Vec<Vec<Point>>,
129	/// The srid.
130	pub srid: i32,
131}
132
133impl Polygon {
134	/// Calculate area of the polygon using the shoelace formula
135	/// Takes into account interior rings (holes) if present
136	///
137	pub fn area(&self) -> f64 {
138		if self.exterior.len() < 3 {
139			return 0.0;
140		}
141
142		// Calculate exterior area
143		let exterior_area = calculate_ring_area(&self.exterior);
144
145		// Subtract interior areas (holes)
146		let interior_area: f64 = self
147			.interiors
148			.iter()
149			.map(|ring| calculate_ring_area(ring))
150			.sum();
151
152		(exterior_area - interior_area).abs()
153	}
154	/// Calculate the exterior boundary area only (ignoring holes)
155	///
156	pub fn exterior_area(&self) -> f64 {
157		calculate_ring_area(&self.exterior)
158	}
159	/// Calculate perimeter length
160	///
161	pub fn perimeter(&self) -> f64 {
162		let mut total = calculate_ring_perimeter(&self.exterior);
163		for interior in &self.interiors {
164			total += calculate_ring_perimeter(interior);
165		}
166		total
167	}
168	/// Check if a point is inside the polygon (simple implementation)
169	///
170	pub fn contains_point(&self, point: &Point) -> bool {
171		point_in_polygon(point, &self.exterior)
172	}
173}
174
175/// Calculate area of a ring using the shoelace formula
176fn calculate_ring_area(ring: &[Point]) -> f64 {
177	if ring.len() < 3 {
178		return 0.0;
179	}
180
181	let mut area = 0.0;
182	for i in 0..ring.len() {
183		let j = (i + 1) % ring.len();
184		area += ring[i].x * ring[j].y;
185		area -= ring[j].x * ring[i].y;
186	}
187	(area / 2.0).abs()
188}
189
190/// Calculate perimeter of a ring
191fn calculate_ring_perimeter(ring: &[Point]) -> f64 {
192	if ring.len() < 2 {
193		return 0.0;
194	}
195
196	let mut total = 0.0;
197	for i in 0..ring.len() {
198		let j = (i + 1) % ring.len();
199		let dx = ring[j].x - ring[i].x;
200		let dy = ring[j].y - ring[i].y;
201		total += (dx * dx + dy * dy).sqrt();
202	}
203	total
204}
205
206/// Point-in-polygon test using ray casting algorithm
207fn point_in_polygon(point: &Point, polygon: &[Point]) -> bool {
208	let mut inside = false;
209	let n = polygon.len();
210
211	for i in 0..n {
212		let j = (i + 1) % n;
213		let pi = &polygon[i];
214		let pj = &polygon[j];
215
216		if ((pi.y > point.y) != (pj.y > point.y))
217			&& (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)
218		{
219			inside = !inside;
220		}
221	}
222
223	inside
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227/// Represents a multi point.
228pub struct MultiPoint {
229	/// The points.
230	pub points: Vec<Point>,
231	/// The srid.
232	pub srid: i32,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236/// Represents a multi line string.
237pub struct MultiLineString {
238	/// The lines.
239	pub lines: Vec<LineString>,
240	/// The srid.
241	pub srid: i32,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245/// Represents a multi polygon.
246pub struct MultiPolygon {
247	/// The polygons.
248	pub polygons: Vec<Polygon>,
249	/// The srid.
250	pub srid: i32,
251}
252
253// ============= SPATIAL OPERATIONS =============
254
255/// Trait defining spatial ops behavior.
256pub trait SpatialOps {
257	/// Check if geometry contains another
258	fn contains(&self, other: &dyn SpatialOps) -> bool;
259
260	/// Check if geometries intersect
261	fn intersects(&self, other: &dyn SpatialOps) -> bool;
262
263	/// Check if geometry is within another
264	fn within(&self, other: &dyn SpatialOps) -> bool;
265
266	/// Check if geometries touch
267	fn touches(&self, other: &dyn SpatialOps) -> bool;
268
269	/// Calculate distance
270	fn distance(&self, other: &dyn SpatialOps) -> f64;
271
272	/// Get bounding box
273	fn bbox(&self) -> BoundingBox;
274}
275
276#[derive(Debug, Clone)]
277/// Represents a bounding box.
278pub struct BoundingBox {
279	/// The min x.
280	pub min_x: f64,
281	/// The min y.
282	pub min_y: f64,
283	/// The max x.
284	pub max_x: f64,
285	/// The max y.
286	pub max_y: f64,
287}
288
289impl BoundingBox {
290	/// Check if bounding box contains a point
291	///
292	pub fn contains_point(&self, point: &Point) -> bool {
293		point.x >= self.min_x
294			&& point.x <= self.max_x
295			&& point.y >= self.min_y
296			&& point.y <= self.max_y
297	}
298	/// Check if two bounding boxes intersect
299	///
300	pub fn intersects(&self, other: &BoundingBox) -> bool {
301		!(self.max_x < other.min_x
302			|| self.min_x > other.max_x
303			|| self.max_y < other.min_y
304			|| self.min_y > other.max_y)
305	}
306}
307
308// ============= SPATIAL QUERIES =============
309
310/// Defines possible spatial lookup values.
311pub enum SpatialLookup {
312	/// Contains variant.
313	Contains(Point),
314	/// Within variant.
315	Within(Polygon),
316	/// Intersects variant.
317	Intersects(Polygon),
318	/// DWithin variant.
319	DWithin(Point, f64), // Distance within
320	/// BBContains variant.
321	BBContains(BoundingBox),
322	/// BBOverlaps variant.
323	BBOverlaps(BoundingBox),
324}
325
326impl SpatialLookup {
327	/// Documentation for `to_sql`
328	///
329	pub fn to_sql(&self) -> String {
330		match self {
331			SpatialLookup::Contains(point) => {
332				format!(
333					"ST_Contains(geometry, ST_GeomFromText('POINT({} {})', 4326))",
334					point.x, point.y
335				)
336			}
337			SpatialLookup::DWithin(point, distance) => {
338				format!(
339					"ST_DWithin(geometry, ST_GeomFromText('POINT({} {})', 4326), {})",
340					point.x, point.y, distance
341				)
342			}
343			SpatialLookup::Within(polygon) => {
344				let coords = polygon
345					.exterior
346					.iter()
347					.map(|p| format!("{} {}", p.x, p.y))
348					.collect::<Vec<_>>()
349					.join(", ");
350				format!(
351					"ST_Within(geometry, ST_GeomFromText('POLYGON(({})))', 4326)",
352					coords
353				)
354			}
355			SpatialLookup::Intersects(polygon) => {
356				let coords = polygon
357					.exterior
358					.iter()
359					.map(|p| format!("{} {}", p.x, p.y))
360					.collect::<Vec<_>>()
361					.join(", ");
362				format!(
363					"ST_Intersects(geometry, ST_GeomFromText('POLYGON(({})))', 4326)",
364					coords
365				)
366			}
367			SpatialLookup::BBContains(bbox) => {
368				format!(
369					"geometry && ST_MakeEnvelope({}, {}, {}, {}, 4326)",
370					bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y
371				)
372			}
373			SpatialLookup::BBOverlaps(bbox) => {
374				format!(
375					"ST_Overlaps(geometry, ST_MakeEnvelope({}, {}, {}, {}, 4326))",
376					bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y
377				)
378			}
379		}
380	}
381}
382
383// ============= COORDINATE SYSTEMS =============
384
385/// Represents a coordinate transform.
386pub struct CoordinateTransform {
387	/// The from srid.
388	pub from_srid: i32,
389	/// The to srid.
390	pub to_srid: i32,
391}
392
393impl CoordinateTransform {
394	/// Create a coordinate system transformation between SRIDs
395	///
396	/// # Examples
397	///
398	/// ```
399	/// use reinhardt_db::orm::gis::CoordinateTransform;
400	///
401	/// let transform = CoordinateTransform::new(4326, 3857);
402	/// assert_eq!(transform.from_srid, 4326); // WGS 84
403	/// assert_eq!(transform.to_srid, 3857);   // Web Mercator
404	/// // Converts GPS coordinates to map projection
405	/// ```
406	pub fn new(from_srid: i32, to_srid: i32) -> Self {
407		Self { from_srid, to_srid }
408	}
409	/// Transform point coordinates
410	/// Supports common transformations: WGS84 (4326) <-> Web Mercator (3857)
411	/// For other transformations, consider integrating with PROJ library
412	///
413	pub fn transform_point(&self, point: &Point) -> Point {
414		// WGS84 to Web Mercator (EPSG:3857)
415		if self.from_srid == 4326 && self.to_srid == 3857 {
416			return wgs84_to_web_mercator(point);
417		}
418
419		// Web Mercator to WGS84
420		if self.from_srid == 3857 && self.to_srid == 4326 {
421			return web_mercator_to_wgs84(point);
422		}
423
424		// Same SRID - no transformation needed
425		if self.from_srid == self.to_srid {
426			return point.clone();
427		}
428
429		// For unsupported transformations, return original point with updated SRID
430		// In production, this should integrate with PROJ library
431		eprintln!(
432			"Warning: Coordinate transformation from SRID {} to {} is not implemented. \
433             Consider using PROJ library for full support.",
434			self.from_srid, self.to_srid
435		);
436
437		Point {
438			x: point.x,
439			y: point.y,
440			srid: self.to_srid,
441		}
442	}
443	/// Get SQL for ST_Transform function
444	///
445	pub fn to_sql(&self, geometry_expr: &str) -> String {
446		format!("ST_Transform({}, {})", geometry_expr, self.to_srid)
447	}
448}
449
450/// Convert WGS84 (EPSG:4326) coordinates to Web Mercator (EPSG:3857)
451fn wgs84_to_web_mercator(point: &Point) -> Point {
452	const EARTH_RADIUS: f64 = 6378137.0; // Earth radius in meters
453
454	let lon = point.x;
455	let lat = point.y;
456
457	// Clamp latitude to avoid infinity
458	let lat = lat.clamp(-85.0511, 85.0511);
459
460	let x = lon.to_radians() * EARTH_RADIUS;
461	let y = ((std::f64::consts::PI / 4.0 + lat.to_radians() / 2.0).tan()).ln() * EARTH_RADIUS;
462
463	Point { x, y, srid: 3857 }
464}
465
466/// Convert Web Mercator (EPSG:3857) coordinates to WGS84 (EPSG:4326)
467fn web_mercator_to_wgs84(point: &Point) -> Point {
468	const EARTH_RADIUS: f64 = 6378137.0;
469
470	let x = point.x;
471	let y = point.y;
472
473	let lon = (x / EARTH_RADIUS).to_degrees();
474	let lat = (2.0 * ((y / EARTH_RADIUS).exp()).atan() - std::f64::consts::PI / 2.0).to_degrees();
475
476	Point {
477		x: lon,
478		y: lat,
479		srid: 4326,
480	}
481}
482
483// ============= SPATIAL INDEXES =============
484
485/// Represents a gi stindex.
486pub struct GiSTIndex {
487	/// The column.
488	pub column: String,
489}
490
491impl GiSTIndex {
492	/// Create a GiST spatial index for geometry columns
493	///
494	/// # Examples
495	///
496	/// ```
497	/// use reinhardt_db::orm::gis::GiSTIndex;
498	///
499	/// let index = GiSTIndex::new("location");
500	/// assert_eq!(index.column, "location");
501	/// // GiST indexes enable fast spatial queries
502	/// ```
503	pub fn new(column: impl Into<String>) -> Self {
504		Self {
505			column: column.into(),
506		}
507	}
508	/// Documentation for `create_sql`
509	///
510	pub fn create_sql(&self, table: &str, index_name: &str) -> String {
511		format!(
512			"CREATE INDEX {} ON {} USING GIST ({})",
513			index_name, table, self.column
514		)
515	}
516}
517
518// ============= MEASUREMENTS =============
519
520/// Represents a distance.
521pub struct Distance {
522	value: f64,
523	unit: DistanceUnit,
524}
525
526#[derive(Debug, Clone, Copy)]
527/// Defines possible distance unit values.
528pub enum DistanceUnit {
529	/// Meters variant.
530	Meters,
531	/// Kilometers variant.
532	Kilometers,
533	/// Miles variant.
534	Miles,
535	/// Feet variant.
536	Feet,
537}
538
539impl Distance {
540	/// Create a distance value with specified unit
541	///
542	/// # Examples
543	///
544	/// ```
545	/// use reinhardt_db::orm::gis::{Distance, DistanceUnit};
546	///
547	/// let distance = Distance::new(1500.0, DistanceUnit::Meters);
548	/// // Represents 1.5 kilometers
549	/// ```
550	pub fn new(value: f64, unit: DistanceUnit) -> Self {
551		Self { value, unit }
552	}
553	/// Documentation for `km`
554	///
555	pub fn km(value: f64) -> Self {
556		Self::new(value, DistanceUnit::Kilometers)
557	}
558	/// Documentation for `m`
559	///
560	pub fn m(value: f64) -> Self {
561		Self::new(value, DistanceUnit::Meters)
562	}
563	/// Documentation for `mi`
564	///
565	pub fn mi(value: f64) -> Self {
566		Self::new(value, DistanceUnit::Miles)
567	}
568	/// Documentation for `ft`
569	///
570	pub fn ft(value: f64) -> Self {
571		Self::new(value, DistanceUnit::Feet)
572	}
573	/// Documentation for `to_meters`
574	///
575	pub fn to_meters(&self) -> f64 {
576		match self.unit {
577			DistanceUnit::Meters => self.value,
578			DistanceUnit::Kilometers => self.value * 1000.0,
579			DistanceUnit::Miles => self.value * 1609.34,
580			DistanceUnit::Feet => self.value * 0.3048,
581		}
582	}
583}
584
585// Spatial aggregates implementation
586/// Represents a spatial aggregate.
587pub struct SpatialAggregate;
588
589impl SpatialAggregate {
590	/// Generate SQL for ST_Union aggregate
591	///
592	pub fn union_sql(column: &str) -> String {
593		format!("ST_Union({})", column)
594	}
595	/// Generate SQL for ST_Collect aggregate
596	///
597	pub fn collect_sql(column: &str) -> String {
598		format!("ST_Collect({})", column)
599	}
600	/// Generate SQL for ST_Extent aggregate
601	///
602	pub fn extent_sql(column: &str) -> String {
603		format!("ST_Extent({})", column)
604	}
605}
606// - Measure operations (Area, Length, Perimeter)
607// - Simplify, Buffer, Centroid
608// - GeoJSON, WKT, WKB format support
609
610#[cfg(test)]
611mod tests {
612	use super::*;
613
614	#[test]
615	fn test_point_creation() {
616		let point = Point::new(10.0, 20.0);
617		assert_eq!(point.x, 10.0);
618		assert_eq!(point.y, 20.0);
619		assert_eq!(point.srid, 4326);
620	}
621
622	#[test]
623	fn test_point_distance() {
624		// Use Cartesian coordinates (not WGS84) for simple Euclidean distance test
625		let p1 = Point::with_srid(0.0, 0.0, 0);
626		let p2 = Point::with_srid(3.0, 4.0, 0);
627		assert_eq!(p1.distance_to(&p2), 5.0);
628	}
629
630	#[test]
631	fn test_distance_conversions() {
632		let d = Distance::km(1.0);
633		assert_eq!(d.to_meters(), 1000.0);
634
635		let d2 = Distance::mi(1.0);
636		assert!((d2.to_meters() - 1609.34).abs() < 0.01);
637	}
638
639	#[test]
640	fn test_gist_index_sql() {
641		let index = GiSTIndex::new("location");
642		let sql = index.create_sql("places", "places_location_idx");
643		assert!(sql.contains("GIST"));
644		assert!(sql.contains("location"));
645	}
646
647	#[test]
648	fn test_spatial_lookup_sql() {
649		let point = Point::new(10.0, 20.0);
650		let lookup = SpatialLookup::Contains(point);
651		let sql = lookup.to_sql();
652		assert!(sql.contains("ST_Contains"));
653		assert!(sql.contains("POINT(10 20)"));
654	}
655
656	// Additional comprehensive GIS tests
657	#[test]
658	fn test_point_with_custom_srid() {
659		let point = Point::with_srid(10.0, 20.0, 3857);
660		assert_eq!(point.srid, 3857);
661		assert_eq!(point.x, 10.0);
662		assert_eq!(point.y, 20.0);
663	}
664
665	#[test]
666	fn test_linestring_length() {
667		let line = LineString {
668			points: vec![
669				Point::new(0.0, 0.0),
670				Point::new(3.0, 0.0),
671				Point::new(3.0, 4.0),
672			],
673			srid: 4326,
674		};
675		assert_eq!(line.length(), 7.0); // 3 + 4
676	}
677
678	#[test]
679	fn test_linestring_empty() {
680		let line = LineString {
681			points: vec![],
682			srid: 4326,
683		};
684		assert_eq!(line.length(), 0.0);
685	}
686
687	#[test]
688	fn test_polygon_area() {
689		let polygon = Polygon {
690			exterior: vec![
691				Point::new(0.0, 0.0),
692				Point::new(4.0, 0.0),
693				Point::new(4.0, 3.0),
694				Point::new(0.0, 3.0),
695				Point::new(0.0, 0.0),
696			],
697			interiors: vec![],
698			srid: 4326,
699		};
700		assert_eq!(polygon.area(), 12.0);
701	}
702
703	#[test]
704	fn test_polygon_with_hole() {
705		let polygon = Polygon {
706			exterior: vec![
707				Point::new(0.0, 0.0),
708				Point::new(10.0, 0.0),
709				Point::new(10.0, 10.0),
710				Point::new(0.0, 10.0),
711				Point::new(0.0, 0.0),
712			],
713			interiors: vec![vec![
714				Point::new(2.0, 2.0),
715				Point::new(8.0, 2.0),
716				Point::new(8.0, 8.0),
717				Point::new(2.0, 8.0),
718				Point::new(2.0, 2.0),
719			]],
720			srid: 4326,
721		};
722		assert_eq!(polygon.interiors.len(), 1);
723		assert_eq!(polygon.interiors[0].len(), 5);
724	}
725
726	#[test]
727	fn test_multipoint_creation() {
728		let mp = MultiPoint {
729			points: vec![
730				Point::new(0.0, 0.0),
731				Point::new(1.0, 1.0),
732				Point::new(2.0, 2.0),
733			],
734			srid: 4326,
735		};
736		assert_eq!(mp.points.len(), 3);
737	}
738
739	#[test]
740	fn test_bounding_box_contains_point() {
741		let bbox = BoundingBox {
742			min_x: 0.0,
743			min_y: 0.0,
744			max_x: 10.0,
745			max_y: 10.0,
746		};
747
748		assert!(bbox.contains_point(&Point::new(5.0, 5.0)));
749		assert!(bbox.contains_point(&Point::new(0.0, 0.0)));
750		assert!(bbox.contains_point(&Point::new(10.0, 10.0)));
751		assert!(!bbox.contains_point(&Point::new(-1.0, 5.0)));
752		assert!(!bbox.contains_point(&Point::new(11.0, 5.0)));
753		assert!(!bbox.contains_point(&Point::new(5.0, 11.0)));
754	}
755
756	#[test]
757	fn test_bounding_box_intersects() {
758		let bbox1 = BoundingBox {
759			min_x: 0.0,
760			min_y: 0.0,
761			max_x: 10.0,
762			max_y: 10.0,
763		};
764
765		let bbox2 = BoundingBox {
766			min_x: 5.0,
767			min_y: 5.0,
768			max_x: 15.0,
769			max_y: 15.0,
770		};
771
772		assert!(bbox1.intersects(&bbox2));
773
774		let bbox3 = BoundingBox {
775			min_x: 20.0,
776			min_y: 20.0,
777			max_x: 30.0,
778			max_y: 30.0,
779		};
780
781		assert!(!bbox1.intersects(&bbox3));
782	}
783
784	#[test]
785	fn test_distance_meters() {
786		let d = Distance::m(500.0);
787		assert_eq!(d.to_meters(), 500.0);
788	}
789
790	#[test]
791	fn test_distance_feet() {
792		let d = Distance::ft(100.0);
793		assert!((d.to_meters() - 30.48).abs() < 0.01);
794	}
795
796	#[test]
797	fn test_coordinate_transform_creation() {
798		let transform = CoordinateTransform {
799			from_srid: 4326,
800			to_srid: 3857,
801		};
802		assert_eq!(transform.from_srid, 4326);
803		assert_eq!(transform.to_srid, 3857);
804	}
805
806	#[test]
807	fn test_spatial_lookup_contains() {
808		let point = Point::new(5.0, 5.0);
809		let lookup = SpatialLookup::Contains(point);
810		matches!(lookup, SpatialLookup::Contains(_));
811	}
812
813	#[test]
814	fn test_spatial_lookup_within() {
815		let polygon = Polygon {
816			exterior: vec![
817				Point::new(0.0, 0.0),
818				Point::new(10.0, 0.0),
819				Point::new(10.0, 10.0),
820				Point::new(0.0, 10.0),
821				Point::new(0.0, 0.0),
822			],
823			interiors: vec![],
824			srid: 4326,
825		};
826		let lookup = SpatialLookup::Within(polygon);
827		matches!(lookup, SpatialLookup::Within(_));
828	}
829
830	#[test]
831	fn test_spatial_lookup_intersects() {
832		let polygon = Polygon {
833			exterior: vec![
834				Point::new(0.0, 0.0),
835				Point::new(10.0, 0.0),
836				Point::new(10.0, 10.0),
837				Point::new(0.0, 10.0),
838				Point::new(0.0, 0.0),
839			],
840			interiors: vec![],
841			srid: 4326,
842		};
843		let lookup = SpatialLookup::Intersects(polygon);
844		matches!(lookup, SpatialLookup::Intersects(_));
845	}
846
847	#[test]
848	fn test_spatial_lookup_dwithin() {
849		let point = Point::new(0.0, 0.0);
850		let lookup = SpatialLookup::DWithin(point, 1000.0);
851		matches!(lookup, SpatialLookup::DWithin(_, _));
852	}
853
854	#[test]
855	fn test_gist_index_with_different_column() {
856		let index = GiSTIndex::new("geometry");
857		let sql = index.create_sql("shapes", "shapes_geom_idx");
858		assert!(sql.contains("geometry"));
859		assert!(sql.contains("shapes"));
860		assert!(sql.contains("shapes_geom_idx"));
861	}
862
863	#[test]
864	fn test_multilinestring_total_length() {
865		let mls = MultiLineString {
866			lines: vec![
867				LineString {
868					points: vec![Point::new(0.0, 0.0), Point::new(5.0, 0.0)],
869					srid: 4326,
870				},
871				LineString {
872					points: vec![Point::new(0.0, 0.0), Point::new(0.0, 3.0)],
873					srid: 4326,
874				},
875			],
876			srid: 4326,
877		};
878
879		let total_length: f64 = mls.lines.iter().map(|l| l.length()).sum();
880		assert_eq!(total_length, 8.0); // 5 + 3
881	}
882
883	#[test]
884	fn test_multipolygon_total_area() {
885		let mp = MultiPolygon {
886			polygons: vec![
887				Polygon {
888					exterior: vec![
889						Point::new(0.0, 0.0),
890						Point::new(5.0, 0.0),
891						Point::new(5.0, 5.0),
892						Point::new(0.0, 5.0),
893						Point::new(0.0, 0.0),
894					],
895					interiors: vec![],
896					srid: 4326,
897				},
898				Polygon {
899					exterior: vec![
900						Point::new(10.0, 10.0),
901						Point::new(13.0, 10.0),
902						Point::new(13.0, 14.0),
903						Point::new(10.0, 14.0),
904						Point::new(10.0, 10.0),
905					],
906					interiors: vec![],
907					srid: 4326,
908				},
909			],
910			srid: 4326,
911		};
912
913		let total_area = mp.polygons.iter().map(|p| p.area()).sum::<f64>();
914		assert_eq!(total_area, 37.0); // 25 + 12
915	}
916
917	#[test]
918	fn test_point_distance_negative_coords() {
919		// Use Cartesian coordinates (not WGS84) for simple Euclidean distance test
920		let p1 = Point::with_srid(-3.0, -4.0, 0);
921		let p2 = Point::with_srid(0.0, 0.0, 0);
922		assert_eq!(p1.distance_to(&p2), 5.0);
923	}
924
925	#[test]
926	fn test_point_distance_same_point() {
927		let p = Point::new(5.0, 5.0);
928		assert_eq!(p.distance_to(&p), 0.0);
929	}
930
931	#[test]
932	fn test_linestring_single_point() {
933		let line = LineString {
934			points: vec![Point::new(0.0, 0.0)],
935			srid: 4326,
936		};
937		assert_eq!(line.length(), 0.0);
938	}
939
940	#[test]
941	fn test_complex_polygon_with_multiple_holes() {
942		let polygon = Polygon {
943			exterior: vec![
944				Point::new(0.0, 0.0),
945				Point::new(20.0, 0.0),
946				Point::new(20.0, 20.0),
947				Point::new(0.0, 20.0),
948				Point::new(0.0, 0.0),
949			],
950			interiors: vec![
951				vec![
952					Point::new(2.0, 2.0),
953					Point::new(5.0, 2.0),
954					Point::new(5.0, 5.0),
955					Point::new(2.0, 5.0),
956					Point::new(2.0, 2.0),
957				],
958				vec![
959					Point::new(10.0, 10.0),
960					Point::new(15.0, 10.0),
961					Point::new(15.0, 15.0),
962					Point::new(10.0, 15.0),
963					Point::new(10.0, 10.0),
964				],
965			],
966			srid: 4326,
967		};
968
969		assert_eq!(polygon.interiors.len(), 2);
970		// Area = 20*20 - 3*3 - 5*5 = 400 - 9 - 25 = 366
971		assert_eq!(polygon.area(), 366.0);
972	}
973
974	#[test]
975	fn test_spatial_reference_systems() {
976		let wgs84_point = Point::new(139.6917, 35.6895); // Tokyo in WGS84
977		assert_eq!(wgs84_point.srid, 4326);
978
979		let web_mercator_point = Point::with_srid(15540445.0, 4253018.0, 3857);
980		assert_eq!(web_mercator_point.srid, 3857);
981	}
982
983	#[test]
984	fn test_distance_unit_conversions() {
985		let km = Distance::km(1.0);
986		let m = Distance::m(1000.0);
987
988		assert_eq!(km.to_meters(), 1000.0);
989		assert_eq!(m.to_meters(), 1000.0);
990	}
991
992	#[test]
993	fn test_wgs84_to_web_mercator() {
994		// Test transformation from WGS84 to Web Mercator
995		let wgs84_point = Point::with_srid(0.0, 0.0, 4326); // Equator at prime meridian
996		let transform = CoordinateTransform::new(4326, 3857);
997		let web_mercator = transform.transform_point(&wgs84_point);
998
999		assert_eq!(web_mercator.srid, 3857);
1000		assert!((web_mercator.x - 0.0).abs() < 0.01);
1001		assert!((web_mercator.y - 0.0).abs() < 0.01);
1002	}
1003
1004	#[test]
1005	fn test_web_mercator_to_wgs84() {
1006		// Test transformation from Web Mercator to WGS84
1007		let web_mercator = Point::with_srid(0.0, 0.0, 3857);
1008		let transform = CoordinateTransform::new(3857, 4326);
1009		let wgs84 = transform.transform_point(&web_mercator);
1010
1011		assert_eq!(wgs84.srid, 4326);
1012		assert!((wgs84.x - 0.0).abs() < 0.01);
1013		assert!((wgs84.y - 0.0).abs() < 0.01);
1014	}
1015
1016	#[test]
1017	fn test_coordinate_transform_tokyo() {
1018		// Test Tokyo coordinates (139.6917°E, 35.6895°N)
1019		let tokyo_wgs84 = Point::with_srid(139.6917, 35.6895, 4326);
1020		let transform = CoordinateTransform::new(4326, 3857);
1021		let tokyo_web_mercator = transform.transform_point(&tokyo_wgs84);
1022
1023		assert_eq!(tokyo_web_mercator.srid, 3857);
1024		// Web Mercator values for Tokyo: x ≈ 15550409, y ≈ 4257981
1025		assert!(tokyo_web_mercator.x > 15500000.0 && tokyo_web_mercator.x < 15600000.0);
1026		assert!(tokyo_web_mercator.y > 4200000.0 && tokyo_web_mercator.y < 4300000.0);
1027
1028		// Test reverse transformation
1029		let transform_back = CoordinateTransform::new(3857, 4326);
1030		let tokyo_back = transform_back.transform_point(&tokyo_web_mercator);
1031		assert!((tokyo_back.x - 139.6917).abs() < 0.01);
1032		assert!((tokyo_back.y - 35.6895).abs() < 0.01);
1033	}
1034
1035	#[test]
1036	fn test_polygon_with_holes_area() {
1037		// 100x100 square with 20x20 hole
1038		let polygon = Polygon {
1039			exterior: vec![
1040				Point::new(0.0, 0.0),
1041				Point::new(100.0, 0.0),
1042				Point::new(100.0, 100.0),
1043				Point::new(0.0, 100.0),
1044				Point::new(0.0, 0.0),
1045			],
1046			interiors: vec![vec![
1047				Point::new(40.0, 40.0),
1048				Point::new(60.0, 40.0),
1049				Point::new(60.0, 60.0),
1050				Point::new(40.0, 60.0),
1051				Point::new(40.0, 40.0),
1052			]],
1053			srid: 4326,
1054		};
1055
1056		// Area should be 100*100 - 20*20 = 10000 - 400 = 9600
1057		assert_eq!(polygon.area(), 9600.0);
1058		assert_eq!(polygon.exterior_area(), 10000.0);
1059	}
1060
1061	#[test]
1062	fn test_polygon_perimeter() {
1063		// 3x4 rectangle
1064		let polygon = Polygon {
1065			exterior: vec![
1066				Point::new(0.0, 0.0),
1067				Point::new(3.0, 0.0),
1068				Point::new(3.0, 4.0),
1069				Point::new(0.0, 4.0),
1070				Point::new(0.0, 0.0),
1071			],
1072			interiors: vec![],
1073			srid: 4326,
1074		};
1075
1076		// Perimeter = 2*(3+4) = 14
1077		assert_eq!(polygon.perimeter(), 14.0);
1078	}
1079
1080	#[test]
1081	fn test_point_in_polygon() {
1082		let polygon = Polygon {
1083			exterior: vec![
1084				Point::new(0.0, 0.0),
1085				Point::new(10.0, 0.0),
1086				Point::new(10.0, 10.0),
1087				Point::new(0.0, 10.0),
1088				Point::new(0.0, 0.0),
1089			],
1090			interiors: vec![],
1091			srid: 4326,
1092		};
1093
1094		assert!(polygon.contains_point(&Point::new(5.0, 5.0)));
1095		assert!(!polygon.contains_point(&Point::new(15.0, 5.0)));
1096		assert!(!polygon.contains_point(&Point::new(-1.0, 5.0)));
1097	}
1098
1099	#[test]
1100	fn test_linestring_geodesic_length() {
1101		// Simple line for WGS84
1102		let line = LineString {
1103			points: vec![Point::new(0.0, 0.0), Point::new(0.0, 1.0)],
1104			srid: 4326,
1105		};
1106
1107		let planar = line.length();
1108		let geodesic = line.geodesic_length();
1109
1110		// Geodesic should be significantly different from planar for lat/lon
1111		assert!(geodesic > 100000.0); // ~111km for 1 degree at equator
1112		assert!(planar < geodesic); // planar is just 1.0
1113	}
1114}