1use crate::error::Result;
2use crate::gpkg::gpkg_geometry_to_wkb;
3use geo_traits::{
4 CoordTrait, GeometryCollectionTrait, GeometryTrait, LineStringTrait, MultiLineStringTrait,
5 MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait,
6};
7use rusqlite::functions::{Context, FunctionFlags};
8use rusqlite::types::{Type, ValueRef};
9use rusqlite::{Connection, Error};
10use wkb::reader::Wkb;
11
12#[derive(Clone, Copy)]
13struct Bounds {
14 minx: f64,
15 maxx: f64,
16 miny: f64,
17 maxy: f64,
18}
19
20pub fn register_spatial_functions(conn: &Connection) -> Result<()> {
32 register_st_minx(conn)?;
33 register_st_miny(conn)?;
34 register_st_maxx(conn)?;
35 register_st_maxy(conn)?;
36 register_st_isempty(conn)?;
37 Ok(())
38}
39
40pub(crate) fn register_st_minx(conn: &Connection) -> Result<()> {
41 register_bounds_component(conn, "ST_MinX", |b| b.minx)
42}
43
44pub(crate) fn register_st_miny(conn: &Connection) -> Result<()> {
45 register_bounds_component(conn, "ST_MinY", |b| b.miny)
46}
47
48pub(crate) fn register_st_maxx(conn: &Connection) -> Result<()> {
49 register_bounds_component(conn, "ST_MaxX", |b| b.maxx)
50}
51
52pub(crate) fn register_st_maxy(conn: &Connection) -> Result<()> {
53 register_bounds_component(conn, "ST_MaxY", |b| b.maxy)
54}
55
56pub(crate) fn register_st_isempty(conn: &Connection) -> Result<()> {
57 conn.create_scalar_function(
58 "ST_IsEmpty",
59 1,
60 FunctionFlags::SQLITE_DETERMINISTIC,
61 |ctx| {
62 let wkb = match wkb_from_ctx(ctx)? {
63 Some(wkb) => wkb,
64 None => return Ok(None),
65 };
66 let is_empty = bounds_from_geometry(&wkb).is_none();
67 Ok(Some(i64::from(is_empty)))
68 },
69 )?;
70 Ok(())
71}
72
73fn register_bounds_component<F>(conn: &Connection, name: &str, f: F) -> Result<()>
74where
75 F: Fn(Bounds) -> f64 + Copy + Send + Sync + 'static,
76{
77 conn.create_scalar_function(name, 1, FunctionFlags::SQLITE_DETERMINISTIC, move |ctx| {
78 let wkb = match wkb_from_ctx(ctx)? {
79 Some(wkb) => wkb,
80 None => return Ok(None),
81 };
82 Ok(bounds_from_geometry(&wkb).map(f))
83 })?;
84 Ok(())
85}
86
87fn wkb_from_ctx<'a>(ctx: &'a Context<'a>) -> std::result::Result<Option<Wkb<'a>>, Error> {
88 let value = ctx.get_raw(0);
89 match value {
90 ValueRef::Null => Ok(None),
91 ValueRef::Blob(blob) => {
92 let wkb = gpkg_geometry_to_wkb(blob)
93 .map_err(|err| Error::UserFunctionError(Box::new(err)))?;
94 Ok(Some(wkb))
95 }
96 _ => Err(Error::InvalidFunctionParameterType(0, Type::Blob)),
97 }
98}
99
100fn bounds_from_geometry<G: GeometryTrait<T = f64>>(geom: &G) -> Option<Bounds> {
101 use geo_traits::GeometryType as GeoType;
102
103 let mut bounds: Option<Bounds> = None;
104 match geom.as_type() {
105 GeoType::Point(point) => {
106 if let Some(coord) = point.coord() {
107 add_coord(&mut bounds, &coord);
108 }
109 }
110 GeoType::LineString(line) => {
111 for coord in line.coords() {
112 add_coord(&mut bounds, &coord);
113 }
114 }
115 GeoType::Polygon(poly) => {
116 if let Some(ring) = poly.exterior() {
117 add_line_string(&mut bounds, &ring);
118 }
119 for ring in poly.interiors() {
120 add_line_string(&mut bounds, &ring);
121 }
122 }
123 GeoType::MultiPoint(multi) => {
124 for point in multi.points() {
125 if let Some(coord) = point.coord() {
126 add_coord(&mut bounds, &coord);
127 }
128 }
129 }
130 GeoType::MultiLineString(multi) => {
131 for line in multi.line_strings() {
132 add_line_string(&mut bounds, &line);
133 }
134 }
135 GeoType::MultiPolygon(multi) => {
136 for poly in multi.polygons() {
137 if let Some(ring) = poly.exterior() {
138 add_line_string(&mut bounds, &ring);
139 }
140 for ring in poly.interiors() {
141 add_line_string(&mut bounds, &ring);
142 }
143 }
144 }
145 GeoType::GeometryCollection(collection) => {
146 for sub_geom in collection.geometries() {
147 if let Some(sub_bounds) = bounds_from_geometry(&sub_geom) {
148 merge_bounds(&mut bounds, sub_bounds);
149 }
150 }
151 }
152 GeoType::Rect(_) | GeoType::Triangle(_) | GeoType::Line(_) => {
153 unreachable!()
155 }
156 }
157
158 bounds
159}
160
161fn add_line_string<L: LineStringTrait<T = f64>>(bounds: &mut Option<Bounds>, line: &L) {
162 for coord in line.coords() {
163 add_coord(bounds, &coord);
164 }
165}
166
167fn add_coord<C: CoordTrait<T = f64>>(bounds: &mut Option<Bounds>, coord: &C) {
168 let (x, y) = coord.x_y();
169 match bounds {
170 Some(existing) => {
171 existing.minx = existing.minx.min(x);
172 existing.maxx = existing.maxx.max(x);
173 existing.miny = existing.miny.min(y);
174 existing.maxy = existing.maxy.max(y);
175 }
176 None => {
177 *bounds = Some(Bounds {
178 minx: x,
179 maxx: x,
180 miny: y,
181 maxy: y,
182 });
183 }
184 }
185}
186
187fn merge_bounds(bounds: &mut Option<Bounds>, other: Bounds) {
188 match bounds {
189 Some(existing) => {
190 existing.minx = existing.minx.min(other.minx);
191 existing.maxx = existing.maxx.max(other.maxx);
192 existing.miny = existing.miny.min(other.miny);
193 existing.maxy = existing.maxy.max(other.maxy);
194 }
195 None => *bounds = Some(other),
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::register_spatial_functions;
202 use crate::gpkg::wkb_to_gpkg_geometry;
203 use geo_types::{Geometry, GeometryCollection, MultiLineString, MultiPoint};
204 use geo_types::{LineString, Point};
205 use rusqlite::{Connection, params};
206 use wkb::reader::Wkb;
207
208 fn gpkg_blob_from_geometry<G: geo_traits::GeometryTrait<T = f64>>(
209 geometry: G,
210 ) -> crate::Result<Vec<u8>> {
211 let mut wkb = Vec::new();
212 wkb::writer::write_geometry(&mut wkb, &geometry, &Default::default())?;
213 let wkb = Wkb::try_new(&wkb)?;
214 wkb_to_gpkg_geometry(wkb, 4326)
215 }
216
217 #[test]
218 fn st_bounds_for_point() -> crate::Result<()> {
219 let conn = Connection::open_in_memory()?;
220 register_spatial_functions(&conn)?;
221
222 let point = Point::new(1.5, -2.0);
223 let blob = gpkg_blob_from_geometry(point)?;
224
225 let (minx, maxx, miny, maxy, empty): (f64, f64, f64, f64, i64) = conn.query_row(
226 "SELECT ST_MinX(?1), ST_MaxX(?1), ST_MinY(?1), ST_MaxY(?1), ST_IsEmpty(?1)",
227 params![blob],
228 |row| {
229 Ok((
230 row.get(0)?,
231 row.get(1)?,
232 row.get(2)?,
233 row.get(3)?,
234 row.get(4)?,
235 ))
236 },
237 )?;
238
239 assert_eq!(minx, 1.5);
240 assert_eq!(maxx, 1.5);
241 assert_eq!(miny, -2.0);
242 assert_eq!(maxy, -2.0);
243 assert_eq!(empty, 0);
244 Ok(())
245 }
246
247 #[test]
248 fn st_is_empty_for_empty_linestring() -> crate::Result<()> {
249 let conn = Connection::open_in_memory()?;
250 register_spatial_functions(&conn)?;
251
252 let line: LineString<f64> = LineString::new(Vec::new());
253 let blob = gpkg_blob_from_geometry(line)?;
254
255 let (minx, empty): (Option<f64>, i64) =
256 conn.query_row("SELECT ST_MinX(?1), ST_IsEmpty(?1)", params![blob], |row| {
257 Ok((row.get(0)?, row.get(1)?))
258 })?;
259
260 assert!(minx.is_none());
261 assert_eq!(empty, 1);
262 Ok(())
263 }
264
265 #[test]
266 fn st_bounds_for_multipoint() -> crate::Result<()> {
267 let conn = Connection::open_in_memory()?;
268 register_spatial_functions(&conn)?;
269
270 let mp = MultiPoint::from(vec![Point::new(1.0, 5.0), Point::new(-2.0, 3.0)]);
271 let blob = gpkg_blob_from_geometry(mp)?;
272
273 let (minx, maxx, miny, maxy): (f64, f64, f64, f64) = conn.query_row(
274 "SELECT ST_MinX(?1), ST_MaxX(?1), ST_MinY(?1), ST_MaxY(?1)",
275 params![blob],
276 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
277 )?;
278
279 assert_eq!(minx, -2.0);
280 assert_eq!(maxx, 1.0);
281 assert_eq!(miny, 3.0);
282 assert_eq!(maxy, 5.0);
283 Ok(())
284 }
285
286 #[test]
287 fn st_bounds_for_multilinestring() -> crate::Result<()> {
288 let conn = Connection::open_in_memory()?;
289 register_spatial_functions(&conn)?;
290
291 let line_a = LineString::from(vec![(0.0, 0.0), (2.0, 1.0)]);
292 let line_b = LineString::from(vec![(-3.0, 4.0), (-1.0, 2.0)]);
293 let mls = MultiLineString(vec![line_a, line_b]);
294 let blob = gpkg_blob_from_geometry(mls)?;
295
296 let (minx, maxx, miny, maxy): (f64, f64, f64, f64) = conn.query_row(
297 "SELECT ST_MinX(?1), ST_MaxX(?1), ST_MinY(?1), ST_MaxY(?1)",
298 params![blob],
299 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
300 )?;
301
302 assert_eq!(minx, -3.0);
303 assert_eq!(maxx, 2.0);
304 assert_eq!(miny, 0.0);
305 assert_eq!(maxy, 4.0);
306 Ok(())
307 }
308
309 #[test]
310 fn st_bounds_for_geometry_collection() -> crate::Result<()> {
311 let conn = Connection::open_in_memory()?;
312 register_spatial_functions(&conn)?;
313
314 let point = Geometry::Point(Point::new(5.0, -1.0));
315 let line = Geometry::LineString(LineString::from(vec![(-2.0, 2.0), (1.0, 3.0)]));
316 let collection = GeometryCollection::from(vec![point, line]);
317 let blob = gpkg_blob_from_geometry(collection)?;
318
319 let (minx, maxx, miny, maxy): (f64, f64, f64, f64) = conn.query_row(
320 "SELECT ST_MinX(?1), ST_MaxX(?1), ST_MinY(?1), ST_MaxY(?1)",
321 params![blob],
322 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
323 )?;
324
325 assert_eq!(minx, -2.0);
326 assert_eq!(maxx, 5.0);
327 assert_eq!(miny, -1.0);
328 assert_eq!(maxy, 3.0);
329 Ok(())
330 }
331}