Skip to main content

scirs2_interpolate/
geospatial.rs

1//! Geospatial interpolation methods
2//!
3//! This module provides interpolation methods specifically designed for geospatial data,
4//! including handling of geographic coordinates, projection systems, and spatial
5//! autocorrelation. These methods are optimized for Earth science applications.
6//!
7//! # Geospatial Features
8//!
9//! - **Geographic coordinate handling**: Proper handling of longitude/latitude data
10//! - **Projection-aware interpolation**: Support for various map projections
11//! - **Spatial autocorrelation**: Methods that account for spatial dependencies
12//! - **Spherical interpolation**: Interpolation on sphere surface (great circle distances)
13//! - **Elevation-aware interpolation**: 3D interpolation with topographic considerations
14//! - **Boundary handling**: Coastal boundaries and land/water transitions
15//! - **Multi-scale interpolation**: From local to global scale interpolation
16//!
17//! # Examples
18//!
19//! ```rust
20//! use scirs2_core::ndarray::Array1;
21//! use scirs2_interpolate::geospatial::{
22//!     GeospatialInterpolator, CoordinateSystem, InterpolationModel
23//! };
24//!
25//! // Create latitude/longitude coordinates
26//! let latitudes = Array1::from_vec(vec![40.7128, 34.0522, 41.8781, 29.7604]);
27//! let longitudes = Array1::from_vec(vec![-74.0060, -118.2437, -87.6298, -95.3698]);
28//! let temperatures = Array1::from_vec(vec![15.0, 22.0, 12.0, 28.0]);
29//!
30//! // Create geospatial interpolator
31//! let mut interpolator = GeospatialInterpolator::new()
32//!     .with_coordinate_system(CoordinateSystem::WGS84)
33//!     .with_interpolation_model(InterpolationModel::Kriging)
34//!     .with_spherical_distance(true);
35//!
36//! // Fit the interpolator
37//! interpolator.fit(&latitudes.view(), &longitudes.view(), &temperatures.view()).expect("Operation failed");
38//!
39//! // Interpolate at new locations
40//! let query_lats = Array1::from_vec(vec![37.7749, 40.0]);
41//! let query_lons = Array1::from_vec(vec![-122.4194, -100.0]);
42//! let result = interpolator.interpolate(&query_lats.view(), &query_lons.view()).expect("Operation failed");
43//! ```
44
45use crate::advanced::kriging::{CovarianceFunction, KrigingInterpolator};
46use crate::advanced::rbf::{RBFInterpolator, RBFKernel};
47use crate::advanced::thinplate::ThinPlateSpline;
48use crate::error::{InterpolateError, InterpolateResult};
49use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ScalarOperand};
50use scirs2_core::numeric::{Float, FromPrimitive, ToPrimitive};
51use std::fmt::{Debug, Display, LowerExp};
52use std::ops::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign};
53
54/// Coordinate reference systems supported for geospatial interpolation
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum CoordinateSystem {
57    /// World Geodetic System 1984 (latitude/longitude)
58    WGS84,
59    /// Universal Transverse Mercator
60    UTM(u8), // UTM zone number
61    /// Web Mercator (used by web mapping services)
62    WebMercator,
63    /// Local coordinate system (Cartesian)
64    LocalCartesian,
65    /// Custom projection (user-defined)
66    Custom,
67}
68
69/// Interpolation models optimized for geospatial data
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub enum InterpolationModel {
72    /// Inverse Distance Weighting with spatial considerations
73    InverseDistanceWeighting,
74    /// Kriging with spatial autocorrelation
75    Kriging,
76    /// Radial Basis Functions with spherical kernels
77    SphericalRBF,
78    /// Thin plate splines adapted for geographic data
79    ThinPlateSpline,
80    /// Natural neighbor interpolation
81    NaturalNeighbor,
82    /// Bilinear interpolation for gridded data
83    Bilinear,
84}
85
86/// Configuration for geospatial interpolation
87#[derive(Debug, Clone)]
88pub struct GeospatialConfig<T> {
89    /// Coordinate reference system
90    pub coordinate_system: CoordinateSystem,
91    /// Interpolation model to use
92    pub model: InterpolationModel,
93    /// Whether to use spherical distance calculations
94    pub use_spherical_distance: bool,
95    /// Earth radius in kilometers (for spherical calculations)
96    pub earth_radius_km: T,
97    /// Search radius for local interpolation methods
98    pub search_radius_km: Option<T>,
99    /// Maximum number of neighbors to consider
100    pub max_neighbors: Option<usize>,
101    /// Anisotropy parameters (direction-dependent scaling)
102    pub anisotropy_angle: Option<T>,
103    pub anisotropy_ratio: Option<T>,
104    /// Elevation weighting factor (if elevation data is provided)
105    pub elevation_weight: Option<T>,
106}
107
108impl<T: Float + FromPrimitive> Default for GeospatialConfig<T> {
109    fn default() -> Self {
110        Self {
111            coordinate_system: CoordinateSystem::WGS84,
112            model: InterpolationModel::Kriging,
113            use_spherical_distance: true,
114            earth_radius_km: T::from_f64(6371.0).expect("Operation failed"), // Mean Earth radius
115            search_radius_km: None,
116            max_neighbors: Some(20),
117            anisotropy_angle: None,
118            anisotropy_ratio: None,
119            elevation_weight: None,
120        }
121    }
122}
123
124/// Result of geospatial interpolation with spatial metadata
125#[derive(Debug, Clone)]
126pub struct GeospatialResult<T> {
127    /// Interpolated values
128    pub values: Array1<T>,
129    /// Interpolation variance/uncertainty (if available)
130    pub variance: Option<Array1<T>>,
131    /// Effective number of neighbors used for each point
132    pub neighbor_counts: Option<Array1<usize>>,
133    /// Spatial autocorrelation metrics
134    pub spatial_correlation: Option<T>,
135}
136
137/// Geospatial interpolator for Earth science data
138#[derive(Debug)]
139pub struct GeospatialInterpolator<T>
140where
141    T: Float
142        + FromPrimitive
143        + ToPrimitive
144        + Debug
145        + Display
146        + LowerExp
147        + ScalarOperand
148        + AddAssign
149        + SubAssign
150        + MulAssign
151        + DivAssign
152        + RemAssign
153        + Copy
154        + 'static,
155{
156    /// Configuration for geospatial interpolation
157    config: GeospatialConfig<T>,
158    /// Training latitudes
159    train_latitudes: Array1<T>,
160    /// Training longitudes
161    train_longitudes: Array1<T>,
162    /// Training values
163    train_values: Array1<T>,
164    /// Optional elevation data
165    #[allow(dead_code)]
166    train_elevations: Option<Array1<T>>,
167    /// Underlying interpolator
168    interpolator: Option<Box<dyn GeospatialInterpolatorTrait<T>>>,
169    /// Whether the model is trained
170    is_trained: bool,
171    /// Computed spatial statistics
172    spatial_stats: SpatialStats<T>,
173}
174
175/// Trait for geospatial interpolation implementations
176trait GeospatialInterpolatorTrait<T>: Debug
177where
178    T: Float + FromPrimitive + ToPrimitive + Debug + Display + Copy + 'static,
179{
180    /// Interpolate at new locations
181    fn interpolate_spatial(
182        &self,
183        latitudes: &ArrayView1<T>,
184        longitudes: &ArrayView1<T>,
185    ) -> InterpolateResult<GeospatialResult<T>>;
186
187    /// Get model-specific parameters
188    #[allow(dead_code)]
189    fn get_parameters(&self) -> Vec<(String, String)>;
190}
191
192/// Statistics about spatial patterns in the data
193#[derive(Debug, Clone)]
194pub struct SpatialStats<T> {
195    /// Estimated spatial range (km)
196    pub spatial_range_km: Option<T>,
197    /// Spatial variance
198    pub spatial_variance: Option<T>,
199    /// Moran's I spatial autocorrelation
200    pub morans_i: Option<T>,
201    /// Effective number of degrees of freedom
202    pub effective_dof: Option<T>,
203    /// Spatial clustering index
204    pub clustering_index: Option<T>,
205}
206
207impl<T> Default for SpatialStats<T> {
208    fn default() -> Self {
209        Self {
210            spatial_range_km: None,
211            spatial_variance: None,
212            morans_i: None,
213            effective_dof: None,
214            clustering_index: None,
215        }
216    }
217}
218
219impl<T> Default for GeospatialInterpolator<T>
220where
221    T: Float
222        + FromPrimitive
223        + ToPrimitive
224        + Debug
225        + Display
226        + LowerExp
227        + ScalarOperand
228        + AddAssign
229        + SubAssign
230        + MulAssign
231        + DivAssign
232        + RemAssign
233        + Copy
234        + Send
235        + Sync
236        + std::iter::Sum
237        + 'static,
238{
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl<T> GeospatialInterpolator<T>
245where
246    T: Float
247        + FromPrimitive
248        + ToPrimitive
249        + Debug
250        + Display
251        + LowerExp
252        + ScalarOperand
253        + AddAssign
254        + SubAssign
255        + MulAssign
256        + DivAssign
257        + RemAssign
258        + Copy
259        + Send
260        + Sync
261        + std::iter::Sum
262        + 'static,
263{
264    /// Create a new geospatial interpolator
265    pub fn new() -> Self {
266        Self {
267            config: GeospatialConfig::default(),
268            train_latitudes: Array1::zeros(0),
269            train_longitudes: Array1::zeros(0),
270            train_values: Array1::zeros(0),
271            train_elevations: None,
272            interpolator: None,
273            is_trained: false,
274            spatial_stats: SpatialStats::default(),
275        }
276    }
277
278    /// Set the coordinate reference system
279    pub fn with_coordinate_system(mut self, coordsys: CoordinateSystem) -> Self {
280        self.config.coordinate_system = coordsys;
281        self
282    }
283
284    /// Set the interpolation model
285    pub fn with_interpolation_model(mut self, model: InterpolationModel) -> Self {
286        self.config.model = model;
287        self
288    }
289
290    /// Enable or disable spherical distance calculations
291    pub fn with_spherical_distance(mut self, usespherical: bool) -> Self {
292        self.config.use_spherical_distance = usespherical;
293        self
294    }
295
296    /// Set search radius for local interpolation methods
297    pub fn with_search_radius_km(mut self, radius: T) -> Self {
298        self.config.search_radius_km = Some(radius);
299        self
300    }
301
302    /// Set maximum number of neighbors
303    pub fn with_max_neighbors(mut self, maxneighbors: usize) -> Self {
304        self.config.max_neighbors = Some(maxneighbors);
305        self
306    }
307
308    /// Set anisotropy parameters for directional spatial correlation
309    pub fn with_anisotropy(mut self, angle: T, ratio: T) -> Self {
310        self.config.anisotropy_angle = Some(angle);
311        self.config.anisotropy_ratio = Some(ratio);
312        self
313    }
314
315    /// Fit the geospatial interpolator to data
316    ///
317    /// # Arguments
318    ///
319    /// * `latitudes` - Latitude coordinates in degrees
320    /// * `longitudes` - Longitude coordinates in degrees  
321    /// * `values` - Values at each coordinate
322    ///
323    /// # Returns
324    ///
325    /// Success indicator
326    pub fn fit(
327        &mut self,
328        latitudes: &ArrayView1<T>,
329        longitudes: &ArrayView1<T>,
330        values: &ArrayView1<T>,
331    ) -> InterpolateResult<()> {
332        if latitudes.len() != longitudes.len() || latitudes.len() != values.len() {
333            return Err(InterpolateError::DimensionMismatch(format!(
334                "latitudes ({}), longitudes ({}), and values ({}) must have the same length",
335                latitudes.len(),
336                longitudes.len(),
337                values.len()
338            )));
339        }
340
341        if latitudes.len() < 3 {
342            return Err(InterpolateError::InvalidValue(
343                "At least 3 data points required for geospatial interpolation".to_string(),
344            ));
345        }
346
347        // Store training data
348        self.train_latitudes = latitudes.to_owned();
349        self.train_longitudes = longitudes.to_owned();
350        self.train_values = values.to_owned();
351
352        // Convert coordinates to projected system if needed
353        let (x_coords, y_coords) = self.project_coordinates(latitudes, longitudes)?;
354
355        // Compute spatial statistics
356        self.compute_spatial_statistics(&x_coords, &y_coords, values)?;
357
358        // Create the appropriate interpolator based on model type
359        match self.config.model {
360            InterpolationModel::Kriging => {
361                self.fit_kriging(&x_coords, &y_coords, values)?;
362            }
363            InterpolationModel::SphericalRBF => {
364                self.fit_spherical_rbf(&x_coords, &y_coords, values)?;
365            }
366            InterpolationModel::ThinPlateSpline => {
367                self.fit_thin_plate_spline(&x_coords, &y_coords, values)?;
368            }
369            _ => {
370                // For other models, use a default RBF approach
371                self.fit_default_rbf(&x_coords, &y_coords, values)?;
372            }
373        }
374
375        self.is_trained = true;
376        Ok(())
377    }
378
379    /// Interpolate at new geographic locations
380    ///
381    /// # Arguments
382    ///
383    /// * `latitudes` - Query latitude coordinates
384    /// * `longitudes` - Query longitude coordinates
385    ///
386    /// # Returns
387    ///
388    /// Geospatial interpolation result
389    pub fn interpolate(
390        &self,
391        latitudes: &ArrayView1<T>,
392        longitudes: &ArrayView1<T>,
393    ) -> InterpolateResult<GeospatialResult<T>> {
394        if !self.is_trained {
395            return Err(InterpolateError::InvalidState(
396                "Interpolator must be fitted before interpolation".to_string(),
397            ));
398        }
399
400        if latitudes.len() != longitudes.len() {
401            return Err(InterpolateError::DimensionMismatch(
402                "latitudes and longitudes must have the same length".to_string(),
403            ));
404        }
405
406        // Project query coordinates
407        let _x_coords_y_coords = self.project_coordinates(latitudes, longitudes)?;
408
409        // Use the fitted interpolator
410        if let Some(ref interpolator) = self.interpolator {
411            interpolator.interpolate_spatial(latitudes, longitudes)
412        } else {
413            Err(InterpolateError::InvalidState(
414                "No interpolator has been fitted".to_string(),
415            ))
416        }
417    }
418
419    /// Project geographic coordinates to the configured coordinate system
420    fn project_coordinates(
421        &self,
422        latitudes: &ArrayView1<T>,
423        longitudes: &ArrayView1<T>,
424    ) -> InterpolateResult<(Array1<T>, Array1<T>)> {
425        match self.config.coordinate_system {
426            CoordinateSystem::WGS84 => {
427                // For WGS84, we can use coordinates directly or convert to radians
428                if self.config.use_spherical_distance {
429                    // Convert to radians for spherical calculations
430                    let lat_rad = latitudes.mapv(|lat| {
431                        lat * T::from_f64(std::f64::consts::PI / 180.0).expect("Operation failed")
432                    });
433                    let lon_rad = longitudes.mapv(|lon| {
434                        lon * T::from_f64(std::f64::consts::PI / 180.0).expect("Operation failed")
435                    });
436                    Ok((lat_rad, lon_rad))
437                } else {
438                    // Use degrees directly
439                    Ok((latitudes.to_owned(), longitudes.to_owned()))
440                }
441            }
442            CoordinateSystem::WebMercator => self.project_to_web_mercator(latitudes, longitudes),
443            CoordinateSystem::LocalCartesian => {
444                // Simple equirectangular projection for local use
445                self.project_equirectangular(latitudes, longitudes)
446            }
447            _ => {
448                // For other coordinate systems, use simple degree coordinates for now
449                Ok((latitudes.to_owned(), longitudes.to_owned()))
450            }
451        }
452    }
453
454    /// Project to Web Mercator coordinates
455    fn project_to_web_mercator(
456        &self,
457        latitudes: &ArrayView1<T>,
458        longitudes: &ArrayView1<T>,
459    ) -> InterpolateResult<(Array1<T>, Array1<T>)> {
460        let earth_radius =
461            self.config.earth_radius_km * T::from_f64(1000.0).expect("Operation failed"); // Convert to meters
462        let deg_to_rad = T::from_f64(std::f64::consts::PI / 180.0).expect("Operation failed");
463
464        let x_coords = longitudes.mapv(|lon| lon * deg_to_rad * earth_radius);
465        let y_coords = latitudes.mapv(|lat| {
466            let lat_rad = lat * deg_to_rad;
467            earth_radius
468                * (lat_rad + T::from_f64(std::f64::consts::PI / 4.0).expect("Operation failed"))
469                    .tan()
470                    .ln()
471        });
472
473        Ok((x_coords, y_coords))
474    }
475
476    /// Simple equirectangular projection
477    fn project_equirectangular(
478        &self,
479        latitudes: &ArrayView1<T>,
480        longitudes: &ArrayView1<T>,
481    ) -> InterpolateResult<(Array1<T>, Array1<T>)> {
482        let deg_to_rad = T::from_f64(std::f64::consts::PI / 180.0).expect("Operation failed");
483        let earth_radius = self.config.earth_radius_km;
484
485        // Use mean latitude for projection scaling
486        let mean_lat = latitudes.sum() / T::from_usize(latitudes.len()).expect("Operation failed");
487        let cos_mean_lat = (mean_lat * deg_to_rad).cos();
488
489        let x_coords = longitudes.mapv(|lon| lon * deg_to_rad * earth_radius * cos_mean_lat);
490        let y_coords = latitudes.mapv(|lat| lat * deg_to_rad * earth_radius);
491
492        Ok((x_coords, y_coords))
493    }
494
495    /// Compute spatial statistics from the data
496    fn compute_spatial_statistics(
497        &mut self,
498        _x_coords: &Array1<T>,
499        _y_coords: &Array1<T>,
500        values: &ArrayView1<T>,
501    ) -> InterpolateResult<()> {
502        // For now, compute basic statistics
503        // In a full implementation, this would include:
504        // - Variogram analysis
505        // - Moran's I calculation
506        // - Range estimation
507        // - Spatial clustering metrics
508
509        let n = values.len();
510        if n > 1 {
511            let mean_val = values.sum() / T::from_usize(n).expect("Operation failed");
512            let variance = values
513                .iter()
514                .map(|&x| (x - mean_val) * (x - mean_val))
515                .sum::<T>()
516                / T::from_usize(n - 1).expect("Operation failed");
517
518            self.spatial_stats.spatial_variance = Some(variance);
519            self.spatial_stats.effective_dof = Some(T::from_usize(n).expect("Operation failed"));
520        }
521
522        Ok(())
523    }
524
525    /// Fit kriging interpolator
526    fn fit_kriging(
527        &mut self,
528        x_coords: &Array1<T>,
529        y_coords: &Array1<T>,
530        values: &ArrayView1<T>,
531    ) -> InterpolateResult<()> {
532        // Create 2D coordinate array for kriging
533        let n = x_coords.len();
534        let mut coords_2d = Array2::zeros((n, 2));
535        for i in 0..n {
536            coords_2d[[i, 0]] = x_coords[i];
537            coords_2d[[i, 1]] = y_coords[i];
538        }
539
540        let kriging = KrigingInterpolator::new(
541            &coords_2d.view(),
542            values,
543            CovarianceFunction::Exponential,
544            T::from_f64(1.0).expect("Operation failed"), // sigma_sq
545            T::from_f64(1.0).expect("Operation failed"), // length_scale
546            T::from_f64(0.01).expect("Operation failed"), // nugget
547            T::from_f64(1.0).expect("Operation failed"), // alpha
548        )?;
549
550        self.interpolator = Some(Box::new(GeospatialKrigingWrapper { kriging }));
551        Ok(())
552    }
553
554    /// Fit spherical RBF interpolator
555    fn fit_spherical_rbf(
556        &mut self,
557        x_coords: &Array1<T>,
558        y_coords: &Array1<T>,
559        values: &ArrayView1<T>,
560    ) -> InterpolateResult<()> {
561        let n = x_coords.len();
562        let mut coords_2d = Array2::zeros((n, 2));
563        for i in 0..n {
564            coords_2d[[i, 0]] = x_coords[i];
565            coords_2d[[i, 1]] = y_coords[i];
566        }
567
568        let rbf = RBFInterpolator::new(
569            &coords_2d.view(),
570            values,
571            RBFKernel::Gaussian,
572            T::from_f64(1.0).expect("Operation failed"),
573        )?;
574
575        self.interpolator = Some(Box::new(GeospatialRBFWrapper { rbf }));
576        Ok(())
577    }
578
579    /// Fit thin plate spline interpolator
580    fn fit_thin_plate_spline(
581        &mut self,
582        x_coords: &Array1<T>,
583        y_coords: &Array1<T>,
584        values: &ArrayView1<T>,
585    ) -> InterpolateResult<()> {
586        let n = x_coords.len();
587        let mut coords_2d = Array2::zeros((n, 2));
588        for i in 0..n {
589            coords_2d[[i, 0]] = x_coords[i];
590            coords_2d[[i, 1]] = y_coords[i];
591        }
592
593        let tps = ThinPlateSpline::new(
594            &coords_2d.view(),
595            values,
596            T::from_f64(0.0).expect("Operation failed"),
597        )?;
598        self.interpolator = Some(Box::new(GeospatialTPSWrapper { tps }));
599        Ok(())
600    }
601
602    /// Fit default RBF interpolator for other models
603    fn fit_default_rbf(
604        &mut self,
605        x_coords: &Array1<T>,
606        y_coords: &Array1<T>,
607        values: &ArrayView1<T>,
608    ) -> InterpolateResult<()> {
609        self.fit_spherical_rbf(x_coords, y_coords, values)
610    }
611
612    /// Calculate great circle distance between two points in kilometers
613    pub fn great_circle_distance(&self, lat1: T, lon1: T, lat2: T, lon2: T) -> T {
614        let deg_to_rad = T::from_f64(std::f64::consts::PI / 180.0).expect("Operation failed");
615        let lat1_rad = lat1 * deg_to_rad;
616        let lon1_rad = lon1 * deg_to_rad;
617        let lat2_rad = lat2 * deg_to_rad;
618        let lon2_rad = lon2 * deg_to_rad;
619
620        let dlat = lat2_rad - lat1_rad;
621        let dlon = lon2_rad - lon1_rad;
622
623        let a = (dlat / T::from_f64(2.0).expect("Operation failed"))
624            .sin()
625            .powi(2)
626            + lat1_rad.cos()
627                * lat2_rad.cos()
628                * (dlon / T::from_f64(2.0).expect("Operation failed"))
629                    .sin()
630                    .powi(2);
631        let c = T::from_f64(2.0).expect("Operation failed") * a.sqrt().asin();
632
633        self.config.earth_radius_km * c
634    }
635
636    /// Get spatial statistics
637    pub fn get_spatial_stats(&self) -> &SpatialStats<T> {
638        &self.spatial_stats
639    }
640
641    /// Check if the interpolator is trained
642    pub fn is_trained(&self) -> bool {
643        self.is_trained
644    }
645}
646
647/// Wrapper for Kriging interpolator to implement GeospatialInterpolatorTrait
648#[derive(Debug)]
649struct GeospatialKrigingWrapper<T>
650where
651    T: Float
652        + FromPrimitive
653        + Debug
654        + Display
655        + std::ops::AddAssign
656        + std::ops::SubAssign
657        + 'static,
658{
659    kriging: KrigingInterpolator<T>,
660}
661
662impl<T> GeospatialInterpolatorTrait<T> for GeospatialKrigingWrapper<T>
663where
664    T: Float
665        + FromPrimitive
666        + ToPrimitive
667        + Debug
668        + Display
669        + Copy
670        + std::ops::AddAssign
671        + std::ops::SubAssign
672        + 'static,
673{
674    fn interpolate_spatial(
675        &self,
676        latitudes: &ArrayView1<T>,
677        longitudes: &ArrayView1<T>,
678    ) -> InterpolateResult<GeospatialResult<T>> {
679        let n = latitudes.len();
680        let mut coords_2d = Array2::zeros((n, 2));
681        for i in 0..n {
682            coords_2d[[i, 0]] = latitudes[i];
683            coords_2d[[i, 1]] = longitudes[i];
684        }
685
686        let result = self.kriging.predict(&coords_2d.view())?;
687        let values = result.value;
688
689        Ok(GeospatialResult {
690            values,
691            variance: None,
692            neighbor_counts: None,
693            spatial_correlation: None,
694        })
695    }
696
697    fn get_parameters(&self) -> Vec<(String, String)> {
698        vec![
699            ("model".to_string(), "Kriging".to_string()),
700            ("covariance".to_string(), "Exponential".to_string()),
701        ]
702    }
703}
704
705/// Wrapper for RBF interpolator to implement GeospatialInterpolatorTrait
706#[derive(Debug)]
707struct GeospatialRBFWrapper<T>
708where
709    T: Float
710        + FromPrimitive
711        + Debug
712        + Display
713        + std::ops::AddAssign
714        + std::ops::SubAssign
715        + std::ops::MulAssign
716        + 'static,
717{
718    rbf: RBFInterpolator<T>,
719}
720
721impl<T> GeospatialInterpolatorTrait<T> for GeospatialRBFWrapper<T>
722where
723    T: Float
724        + FromPrimitive
725        + ToPrimitive
726        + Debug
727        + Display
728        + LowerExp
729        + Copy
730        + Send
731        + Sync
732        + std::ops::AddAssign
733        + std::ops::SubAssign
734        + std::ops::MulAssign
735        + std::ops::DivAssign
736        + 'static,
737{
738    fn interpolate_spatial(
739        &self,
740        latitudes: &ArrayView1<T>,
741        longitudes: &ArrayView1<T>,
742    ) -> InterpolateResult<GeospatialResult<T>> {
743        let n = latitudes.len();
744        let mut coords_2d = Array2::zeros((n, 2));
745        for i in 0..n {
746            coords_2d[[i, 0]] = latitudes[i];
747            coords_2d[[i, 1]] = longitudes[i];
748        }
749
750        let values = self.rbf.interpolate(&coords_2d.view())?;
751
752        Ok(GeospatialResult {
753            values,
754            variance: None,
755            neighbor_counts: None,
756            spatial_correlation: None,
757        })
758    }
759
760    fn get_parameters(&self) -> Vec<(String, String)> {
761        vec![
762            ("model".to_string(), "RBF".to_string()),
763            ("kernel".to_string(), "Gaussian".to_string()),
764        ]
765    }
766}
767
768/// Wrapper for Thin Plate Spline interpolator to implement GeospatialInterpolatorTrait
769#[derive(Debug)]
770struct GeospatialTPSWrapper<T>
771where
772    T: Float
773        + FromPrimitive
774        + Debug
775        + Display
776        + std::ops::AddAssign
777        + std::ops::SubAssign
778        + 'static,
779{
780    tps: ThinPlateSpline<T>,
781}
782
783impl<T> GeospatialInterpolatorTrait<T> for GeospatialTPSWrapper<T>
784where
785    T: Float
786        + FromPrimitive
787        + ToPrimitive
788        + Debug
789        + Display
790        + Copy
791        + std::ops::AddAssign
792        + std::ops::SubAssign
793        + 'static,
794{
795    fn interpolate_spatial(
796        &self,
797        latitudes: &ArrayView1<T>,
798        longitudes: &ArrayView1<T>,
799    ) -> InterpolateResult<GeospatialResult<T>> {
800        let n = latitudes.len();
801        let mut coords_2d = Array2::zeros((n, 2));
802        for i in 0..n {
803            coords_2d[[i, 0]] = latitudes[i];
804            coords_2d[[i, 1]] = longitudes[i];
805        }
806
807        let values = self.tps.evaluate(&coords_2d.view())?;
808
809        Ok(GeospatialResult {
810            values,
811            variance: None,
812            neighbor_counts: None,
813            spatial_correlation: None,
814        })
815    }
816
817    fn get_parameters(&self) -> Vec<(String, String)> {
818        vec![("model".to_string(), "ThinPlateSpline".to_string())]
819    }
820}
821
822/// Convenience function to create a geospatial interpolator for climate data
823#[allow(dead_code)]
824pub fn make_climate_interpolator<T>() -> GeospatialInterpolator<T>
825where
826    T: Float
827        + FromPrimitive
828        + ToPrimitive
829        + Debug
830        + Display
831        + LowerExp
832        + ScalarOperand
833        + AddAssign
834        + SubAssign
835        + MulAssign
836        + DivAssign
837        + RemAssign
838        + Copy
839        + Send
840        + Sync
841        + std::iter::Sum
842        + 'static,
843{
844    GeospatialInterpolator::new()
845        .with_coordinate_system(CoordinateSystem::WGS84)
846        .with_interpolation_model(InterpolationModel::Kriging)
847        .with_spherical_distance(true)
848        .with_max_neighbors(50)
849}
850
851/// Convenience function to create a geospatial interpolator for elevation data
852#[allow(dead_code)]
853pub fn make_elevation_interpolator<T>() -> GeospatialInterpolator<T>
854where
855    T: Float
856        + FromPrimitive
857        + ToPrimitive
858        + Debug
859        + Display
860        + LowerExp
861        + ScalarOperand
862        + AddAssign
863        + SubAssign
864        + MulAssign
865        + DivAssign
866        + RemAssign
867        + Copy
868        + Send
869        + Sync
870        + std::iter::Sum
871        + 'static,
872{
873    GeospatialInterpolator::new()
874        .with_coordinate_system(CoordinateSystem::WGS84)
875        .with_interpolation_model(InterpolationModel::ThinPlateSpline)
876        .with_spherical_distance(true)
877        .with_max_neighbors(30)
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use scirs2_core::ndarray::Array1;
884
885    #[test]
886    fn test_geospatial_interpolator_creation() {
887        let interpolator = GeospatialInterpolator::<f64>::new();
888        assert!(!interpolator.is_trained());
889        assert_eq!(
890            interpolator.config.coordinate_system,
891            CoordinateSystem::WGS84
892        );
893        assert_eq!(interpolator.config.model, InterpolationModel::Kriging);
894    }
895
896    #[test]
897    fn test_geospatial_interpolator_configuration() {
898        let interpolator = GeospatialInterpolator::<f64>::new()
899            .with_coordinate_system(CoordinateSystem::WebMercator)
900            .with_interpolation_model(InterpolationModel::SphericalRBF)
901            .with_spherical_distance(false)
902            .with_max_neighbors(10);
903
904        assert_eq!(
905            interpolator.config.coordinate_system,
906            CoordinateSystem::WebMercator
907        );
908        assert_eq!(interpolator.config.model, InterpolationModel::SphericalRBF);
909        assert!(!interpolator.config.use_spherical_distance);
910        assert_eq!(interpolator.config.max_neighbors, Some(10));
911    }
912
913    #[test]
914    fn test_geospatial_fitting() {
915        let latitudes = Array1::from_vec(vec![40.0, 41.0, 42.0, 43.0]);
916        let longitudes = Array1::from_vec(vec![-74.0, -75.0, -76.0, -77.0]);
917        let temperatures = Array1::from_vec(vec![15.0, 12.0, 10.0, 8.0]);
918
919        let mut interpolator = GeospatialInterpolator::new()
920            .with_interpolation_model(InterpolationModel::SphericalRBF);
921
922        let result = interpolator.fit(&latitudes.view(), &longitudes.view(), &temperatures.view());
923        assert!(result.is_ok());
924        assert!(interpolator.is_trained());
925    }
926
927    #[test]
928    fn test_geospatial_interpolation() {
929        let latitudes = Array1::from_vec(vec![40.0, 41.0, 42.0, 43.0]);
930        let longitudes = Array1::from_vec(vec![-74.0, -75.0, -76.0, -77.0]);
931        let temperatures = Array1::from_vec(vec![15.0, 12.0, 10.0, 8.0]);
932
933        let mut interpolator = GeospatialInterpolator::new()
934            .with_interpolation_model(InterpolationModel::SphericalRBF);
935
936        interpolator
937            .fit(&latitudes.view(), &longitudes.view(), &temperatures.view())
938            .expect("Operation failed");
939
940        let query_lats = Array1::from_vec(vec![40.5, 41.5]);
941        let query_lons = Array1::from_vec(vec![-74.5, -75.5]);
942        let result = interpolator
943            .interpolate(&query_lats.view(), &query_lons.view())
944            .expect("Operation failed");
945
946        assert_eq!(result.values.len(), 2);
947        assert!(result.values.iter().all(|&v| v.is_finite()));
948    }
949
950    #[test]
951    fn test_great_circle_distance() {
952        let interpolator = GeospatialInterpolator::<f64>::new();
953
954        // Distance between NYC and Los Angeles (approximately 3935 km)
955        let nyc_lat = 40.7128;
956        let nyc_lon = -74.0060;
957        let la_lat = 34.0522;
958        let la_lon = -118.2437;
959
960        let distance = interpolator.great_circle_distance(nyc_lat, nyc_lon, la_lat, la_lon);
961
962        // Should be approximately 3935 km (allow 10% error for simple calculation)
963        assert!((distance - 3935.0).abs() < 400.0);
964    }
965
966    #[test]
967    fn test_coordinate_projection() {
968        let interpolator =
969            GeospatialInterpolator::<f64>::new().with_coordinate_system(CoordinateSystem::WGS84);
970
971        let latitudes = Array1::from_vec(vec![40.0, 41.0]);
972        let longitudes = Array1::from_vec(vec![-74.0, -75.0]);
973
974        let result = interpolator.project_coordinates(&latitudes.view(), &longitudes.view());
975        assert!(result.is_ok());
976
977        let (x_coords, y_coords) = result.expect("Operation failed");
978        assert_eq!(x_coords.len(), 2);
979        assert_eq!(y_coords.len(), 2);
980    }
981
982    #[test]
983    fn test_make_climate_interpolator() {
984        let interpolator = make_climate_interpolator::<f64>();
985        assert_eq!(
986            interpolator.config.coordinate_system,
987            CoordinateSystem::WGS84
988        );
989        assert_eq!(interpolator.config.model, InterpolationModel::Kriging);
990        assert!(interpolator.config.use_spherical_distance);
991        assert_eq!(interpolator.config.max_neighbors, Some(50));
992    }
993
994    #[test]
995    fn test_make_elevation_interpolator() {
996        let interpolator = make_elevation_interpolator::<f64>();
997        assert_eq!(
998            interpolator.config.coordinate_system,
999            CoordinateSystem::WGS84
1000        );
1001        assert_eq!(
1002            interpolator.config.model,
1003            InterpolationModel::ThinPlateSpline
1004        );
1005        assert!(interpolator.config.use_spherical_distance);
1006        assert_eq!(interpolator.config.max_neighbors, Some(30));
1007    }
1008
1009    #[test]
1010    fn test_kriging_model() {
1011        let latitudes = Array1::from_vec(vec![40.0, 41.0, 42.0, 43.0, 44.0]);
1012        let longitudes = Array1::from_vec(vec![-74.0, -75.0, -76.0, -77.0, -78.0]);
1013        let temperatures = Array1::from_vec(vec![15.0, 12.0, 10.0, 8.0, 6.0]);
1014
1015        let mut interpolator =
1016            GeospatialInterpolator::new().with_interpolation_model(InterpolationModel::Kriging);
1017
1018        let fit_result =
1019            interpolator.fit(&latitudes.view(), &longitudes.view(), &temperatures.view());
1020        assert!(fit_result.is_ok());
1021
1022        let query_lats = Array1::from_vec(vec![40.5]);
1023        let query_lons = Array1::from_vec(vec![-74.5]);
1024        let result = interpolator.interpolate(&query_lats.view(), &query_lons.view());
1025        assert!(result.is_ok());
1026    }
1027
1028    #[test]
1029    fn test_thin_plate_spline_model() {
1030        // Use non-collinear points for thin plate spline
1031        let latitudes = Array1::from_vec(vec![40.0, 41.0, 40.5, 41.5, 40.8]);
1032        let longitudes = Array1::from_vec(vec![-74.0, -74.5, -75.0, -74.2, -74.8]);
1033        let elevations = Array1::from_vec(vec![100.0, 200.0, 150.0, 250.0, 180.0]);
1034
1035        let mut interpolator = GeospatialInterpolator::new()
1036            .with_interpolation_model(InterpolationModel::ThinPlateSpline);
1037
1038        let fit_result =
1039            interpolator.fit(&latitudes.view(), &longitudes.view(), &elevations.view());
1040        assert!(
1041            fit_result.is_ok(),
1042            "Failed to fit thin plate spline: {:?}",
1043            fit_result.err()
1044        );
1045
1046        let query_lats = Array1::from_vec(vec![40.5]);
1047        let query_lons = Array1::from_vec(vec![-74.5]);
1048        let result = interpolator.interpolate(&query_lats.view(), &query_lons.view());
1049        assert!(result.is_ok());
1050    }
1051}